feat(android-sdk): 添加完整的IM客户端SDK实现
- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能 - 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力 - 实现了群组管理功能,包括创建、成员管理、权限设置等操作 - 添加了好友关系链管理,支持添加、删除、分组等操作 - 实现了会话管理功能,包括置顶、免打扰、已读状态等 - 添加了黑名单、资料管理、搜索等辅助功能 - 补齐了批量操作接口,提升客户端操作效率 - 实现了WebSocket连接管理和事件监听机制 - 添加了离线消息同步和状态管理功能
这个提交包含在:
父节点
f2084c7911
当前提交
8fee092c44
@ -128,6 +128,25 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func sync() {
|
||||||
|
guard let activeAppId else {
|
||||||
|
delegate?.imClientDidError("IM appId not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendFrame(
|
||||||
|
command: "SEND",
|
||||||
|
headers: [
|
||||||
|
"destination": "/app/chat.sync",
|
||||||
|
"content-type": "application/json",
|
||||||
|
],
|
||||||
|
body: encodeJSONString(["appId": activeAppId]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendSync() {
|
||||||
|
sync()
|
||||||
|
}
|
||||||
|
|
||||||
public func subscribeGroup(_ groupId: String) {
|
public func subscribeGroup(_ groupId: String) {
|
||||||
let subscriptionKey = "group-\(groupId)"
|
let subscriptionKey = "group-\(groupId)"
|
||||||
let isNew = groupSubscriptions.insert(groupId).inserted
|
let isNew = groupSubscriptions.insert(groupId).inserted
|
||||||
@ -194,6 +213,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
"id": "group-\(groupId)",
|
"id": "group-\(groupId)",
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
sendSync()
|
||||||
delegate?.imClientDidConnect()
|
delegate?.imClientDidConnect()
|
||||||
case "MESSAGE":
|
case "MESSAGE":
|
||||||
guard let messageData = frame.body.data(using: .utf8),
|
guard let messageData = frame.body.data(using: .utf8),
|
||||||
|
|||||||
@ -908,6 +908,24 @@ public final class ImSDK {
|
|||||||
return conversations.reduce(0) { $0 + $1.unreadCount }
|
return conversations.reduce(0) { $0 + $1.unreadCount }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func offlineMessageCount() async throws -> Int {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let result: [String: Int] = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/messages/offline/count",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
|
||||||
|
)
|
||||||
|
return result["count"] ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func syncOfflineMessages() async throws -> [ImMessage] {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
return try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/messages/offline",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public func adminGroupReadReceipts(groupId: String, messageIds: [String]) async throws -> [GroupReadReceiptSummary] {
|
public func adminGroupReadReceipts(groupId: String, messageIds: [String]) async throws -> [GroupReadReceiptSummary] {
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
return try await ApiClient.shared.request(
|
return try await ApiClient.shared.request(
|
||||||
@ -918,6 +936,96 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func batchAddFriends(friendIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/friends/batch",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchFriendIdsRequest(friendIds: friendIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchRemoveFriends(friendIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/friends/batch/remove",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchFriendIdsRequest(friendIds: friendIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchAcceptFriendRequests(requestIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/friend-requests/batch/accept",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchRequestIdsRequest(requestIds: requestIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchRejectFriendRequests(requestIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/friend-requests/batch/reject",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchRequestIdsRequest(requestIds: requestIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchAddGroupMembers(groupId: String, userIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/groups/\(groupId)/members/batch",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchUserIdsRequest(userIds: userIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchRemoveGroupMembers(groupId: String, userIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/groups/\(groupId)/members/batch/remove",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchUserIdsRequest(userIds: userIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchAcceptGroupJoinRequests(groupId: String, requestIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/groups/\(groupId)/join-requests/batch/accept",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchRequestIdsRequest(requestIds: requestIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func batchRejectGroupJoinRequests(groupId: String, requestIds: [String]) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/groups/\(groupId)/join-requests/batch/reject",
|
||||||
|
method: "POST",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: BatchRequestIdsRequest(requestIds: requestIds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func modifyGroupMemberInfo(groupId: String, userId: String, nickname: String? = nil, role: String? = nil) async throws {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
|
path: "/api/im/groups/\(groupId)/members/\(userId)/info",
|
||||||
|
method: "PUT",
|
||||||
|
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
||||||
|
body: ModifyMemberInfoRequest(nickname: nickname, role: role)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public func disconnect() {
|
public func disconnect() {
|
||||||
client?.disconnect()
|
client?.disconnect()
|
||||||
client = nil
|
client = nil
|
||||||
|
|||||||
@ -224,6 +224,23 @@ public struct ImGroupReadReceiptRequest: Encodable, Sendable {
|
|||||||
public let messageIds: [String]
|
public let messageIds: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct BatchFriendIdsRequest: Encodable, Sendable {
|
||||||
|
public let friendIds: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BatchRequestIdsRequest: Encodable, Sendable {
|
||||||
|
public let requestIds: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BatchUserIdsRequest: Encodable, Sendable {
|
||||||
|
public let userIds: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ModifyMemberInfoRequest: Encodable, Sendable {
|
||||||
|
public let nickname: String?
|
||||||
|
public let role: String?
|
||||||
|
}
|
||||||
|
|
||||||
public enum ImAttributeValue: Codable, Sendable {
|
public enum ImAttributeValue: Codable, Sendable {
|
||||||
case string(String)
|
case string(String)
|
||||||
case int(Int)
|
case int(Int)
|
||||||
|
|||||||
@ -9,10 +9,23 @@ public enum PushVendor: String {
|
|||||||
case fcm = "FCM"
|
case fcm = "FCM"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct PushMessage: Sendable {
|
||||||
|
public let title: String?
|
||||||
|
public let body: String?
|
||||||
|
public let payload: [String: Any]
|
||||||
|
public let rawUserInfo: [AnyHashable: Any]
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol PushMessageDelegate: AnyObject, Sendable {
|
||||||
|
func pushSDK(_ sdk: PushSDK, didReceiveMessage message: PushMessage)
|
||||||
|
func pushSDK(_ sdk: PushSDK, didTapNotification message: PushMessage)
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
public static let shared = PushSDK()
|
public static let shared = PushSDK()
|
||||||
|
public weak var delegate: PushMessageDelegate?
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@ -65,7 +78,7 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
let config = XuqmSDK.shared.requireConfig()
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
let _: EmptyResponse = try await ApiClient.shared.request(
|
let _: EmptyResponse = try await ApiClient.shared.request(
|
||||||
path: "/api/push/unregister",
|
path: "/api/push/unregister",
|
||||||
method: "POST",
|
method: "DELETE",
|
||||||
queryItems: [
|
queryItems: [
|
||||||
URLQueryItem(name: "appId", value: config.appId),
|
URLQueryItem(name: "appId", value: config.appId),
|
||||||
URLQueryItem(name: "userId", value: userId),
|
URLQueryItem(name: "userId", value: userId),
|
||||||
@ -79,6 +92,10 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||||
) {
|
) {
|
||||||
|
let message = parseMessage(notification.request.content)
|
||||||
|
if let message = message {
|
||||||
|
delegate?.pushSDK(self, didReceiveMessage: message)
|
||||||
|
}
|
||||||
completionHandler([.banner, .sound, .badge])
|
completionHandler([.banner, .sound, .badge])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +104,36 @@ public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
|||||||
didReceive response: UNNotificationResponse,
|
didReceive response: UNNotificationResponse,
|
||||||
withCompletionHandler completionHandler: @escaping () -> Void
|
withCompletionHandler completionHandler: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
|
let message = parseMessage(response.notification.request.content)
|
||||||
|
if let message = message {
|
||||||
|
delegate?.pushSDK(self, didTapNotification: message)
|
||||||
|
}
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func parseMessage(_ content: UNNotificationContent) -> PushMessage? {
|
||||||
|
let userInfo = content.userInfo
|
||||||
|
let aps = userInfo["aps"] as? [String: Any]
|
||||||
|
let alert = aps?["alert"] as? [String: Any]
|
||||||
|
let title = content.title.isEmpty ? (alert?["title"] as? String) : content.title
|
||||||
|
let body = content.body.isEmpty ? (alert?["body"] as? String) : content.body
|
||||||
|
return PushMessage(
|
||||||
|
title: title,
|
||||||
|
body: body,
|
||||||
|
payload: userInfo.compactMapKeys { $0 as? String },
|
||||||
|
rawUserInfo: userInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Dictionary {
|
||||||
|
func compactMapKeys<T: Hashable>(_ transform: (Key) -> T?) -> [T: Value] {
|
||||||
|
var result: [T: Value] = [:]
|
||||||
|
for (key, value) in self {
|
||||||
|
if let newKey = transform(key) {
|
||||||
|
result[newKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户