feat(chat): 添加聊天界面视图模型和联系人管理功能

- 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理
- 添加消息搜索、草稿保存、引用回复等功能
- 实现多媒体附件发送包括图片、视频、音频和文件
- 添加群组提及用户功能和消息撤回机制
- 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理
- 添加好友请求处理和实时消息监听
- 实现会话列表管理包含未读消息统计和实时更新
- 集成 IM SDK 的连接状态管理和事件监听
- 添加消息状态跟踪和超时处理机制
- 实现数据缓存机制优化用户体验
这个提交包含在:
XuqmGroup 2026-04-28 22:32:21 +08:00
父节点 5eb46f97c6
当前提交 437e4b042a
共有 3 个文件被更改,包括 77 次插入0 次删除

查看文件

@ -84,6 +84,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
status: .sending,
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
groupReadCount: nil,
revoked: false,
createdAt: now
)
guard let activeAppId else {
@ -199,6 +200,9 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
let msg = try? JSONDecoder().decode(ImMessage.self, from: messageData) else {
continue
}
if (msg.revoked ?? false) || msg.status == .revoked || msg.msgType == .revoked {
delegate?.imClientDidReceiveRevokedMessage(msg)
}
if msg.chatType == .group {
delegate?.imClientDidReceiveGroupMessage(msg)
} else {
@ -298,6 +302,7 @@ private extension ImMessage {
status: .failed,
mentionedUserIds: mentionedUserIds,
groupReadCount: groupReadCount,
revoked: false,
createdAt: createdAt
)
}

查看文件

@ -248,6 +248,16 @@ public final class ImSDK {
client?.revoke(messageId: messageId)
}
public func editMessage(messageId: String, content: String) async throws -> ImMessage {
let config = XuqmSDK.shared.requireConfig()
return try await ApiClient.shared.request(
path: "/api/im/messages/\(messageId)",
method: "PUT",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
body: EditMessageRequest(content: content)
)
}
public func subscribeGroup(_ groupId: String) {
client?.subscribeGroup(groupId)
}
@ -274,6 +284,7 @@ public final class ImSDK {
status: .failed,
mentionedUserIds: mentionedUserIds,
groupReadCount: nil,
revoked: false,
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
)
}
@ -319,6 +330,57 @@ public final class ImSDK {
)
}
public func locateHistoryPage(
toId: String,
messageId: String,
pageSize: Int = 20,
maxPages: Int = 20
) async throws -> [ImMessage]? {
try await locatePage(
messageId: messageId,
maxPages: maxPages,
pageSize: pageSize,
loadPage: { page in
try await self.fetchHistory(toId: toId, page: page, size: pageSize)
}
)
}
public func locateGroupHistoryPage(
groupId: String,
messageId: String,
pageSize: Int = 20,
maxPages: Int = 20
) async throws -> [ImMessage]? {
try await locatePage(
messageId: messageId,
maxPages: maxPages,
pageSize: pageSize,
loadPage: { page in
try await self.fetchGroupHistory(groupId: groupId, page: page, size: pageSize)
}
)
}
private func locatePage(
messageId: String,
maxPages: Int,
pageSize: Int,
loadPage: @escaping (Int) async throws -> [ImMessage]
) async throws -> [ImMessage]? {
let pageCount = max(maxPages, 1)
for page in 0..<pageCount {
let messages = try await loadPage(page)
if messages.contains(where: { $0.id == messageId }) {
return messages
}
if messages.count < pageSize {
return nil
}
}
return nil
}
public func listGroups() async throws -> [ImGroup] {
let config = XuqmSDK.shared.requireConfig()
let groups: [ImGroup] = try await ApiClient.shared.request(

查看文件

@ -43,6 +43,7 @@ public struct ImMessage: Codable, Sendable {
public let status: MsgStatus
public let mentionedUserIds: String?
public let groupReadCount: Int?
public let revoked: Bool?
public let createdAt: Int64
}
@ -63,9 +64,14 @@ public protocol ImEventDelegate: AnyObject {
func imClientDidDisconnect(reason: String?)
func imClientDidReceiveMessage(_ message: ImMessage)
func imClientDidReceiveGroupMessage(_ message: ImMessage)
func imClientDidReceiveRevokedMessage(_ message: ImMessage)
func imClientDidError(_ error: String)
}
public extension ImEventDelegate {
func imClientDidReceiveRevokedMessage(_ message: ImMessage) {}
}
public struct ImGroup: Codable, Sendable {
public let id: String
public let appId: String
@ -139,6 +145,10 @@ public struct ImSendMessageRequest: Encodable, Sendable {
public let mentionedUserIds: String?
}
public struct EditMessageRequest: Encodable, Sendable {
public let content: String
}
public struct ImLoginResponse: Decodable, Sendable {
public let token: String
}