feat(ios): align with Android SDK - STOMP host header, loginWithDemo, Push/Update SDK, add editedAt/fromId, fix MESSAGE frame handling

这个提交包含在:
XuqmGroup 2026-04-30 19:23:22 +08:00
父节点 eaf1c00d20
当前提交 3249e4b4e5
共有 6 个文件被更改,包括 187 次插入15 次删除

查看文件

@ -75,6 +75,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
id: messageId,
appId: activeAppId ?? "",
fromUserId: activeUserId ?? "",
fromId: activeUserId,
toId: toId,
chatType: chatType,
msgType: msgType,
@ -83,7 +84,8 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
groupReadCount: nil,
revoked: false,
createdAt: now
createdAt: now,
editedAt: nil
)
guard let activeAppId else {
delegate?.imClientDidError("IM appId not configured")
@ -198,8 +200,12 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
let msg = try? JSONDecoder().decode(ImMessage.self, from: messageData) else {
continue
}
if msg.status == .read {
delegate?.imClientDidReadMessage(msg)
}
if (msg.revoked ?? false) || msg.status == .revoked || msg.msgType == .revoked {
delegate?.imClientDidReceiveRevokedMessage(msg)
break
}
if msg.chatType == .group {
delegate?.imClientDidReceiveGroupMessage(msg)
@ -272,11 +278,15 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
return
}
activeToken = token
sendFrame(command: "CONNECT", headers: [
var headers: [String: String] = [
"accept-version": "1.2",
"Authorization": "Bearer \(token)",
"heart-beat": "10000,10000",
])
"heart-beat": "0,0",
]
if let host = activeWsURL?.host {
headers["host"] = host
}
sendFrame(command: "CONNECT", headers: headers)
}
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
@ -293,6 +303,7 @@ private extension ImMessage {
id: id,
appId: appId,
fromUserId: fromUserId,
fromId: fromId,
toId: toId,
chatType: chatType,
msgType: msgType,
@ -301,7 +312,8 @@ private extension ImMessage {
mentionedUserIds: mentionedUserIds,
groupReadCount: groupReadCount,
revoked: false,
createdAt: createdAt
createdAt: createdAt,
editedAt: editedAt
)
}
}

查看文件

@ -34,6 +34,33 @@ public final class ImSDK {
client?.connect()
}
public func loginWithDemo(userId: String, password: String = "123456") async throws {
let config = XuqmSDK.shared.requireConfig()
struct DemoLoginResponse: Decodable, Sendable {
let profile: DemoProfile
let imToken: String
struct DemoProfile: Decodable, Sendable {
let appId: String
let userId: String
let nickname: String?
let avatar: String?
}
}
let res: DemoLoginResponse = try await ApiClient.shared.request(
path: "/api/demo/auth/login",
method: "POST",
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
body: ["appId": config.appId, "userId": userId, "password": password]
)
currentUserId = res.profile.userId
XuqmSDK.shared.tokenStore?.save(res.imToken)
client?.disconnect()
client = ImClient(token: res.imToken, appId: config.appId)
client?.setCurrentUserId(res.profile.userId)
client?.delegate = delegate
client?.connect()
}
public func setDelegate(_ delegate: ImEventDelegate) {
self.delegate = delegate
client?.delegate = delegate
@ -282,6 +309,7 @@ public final class ImSDK {
id: UUID().uuidString,
appId: XuqmSDK.shared.requireConfig().appId,
fromUserId: currentUserId ?? "",
fromId: currentUserId,
toId: toId,
chatType: chatType,
msgType: msgType,
@ -290,7 +318,8 @@ public final class ImSDK {
mentionedUserIds: mentionedUserIds,
groupReadCount: nil,
revoked: false,
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
createdAt: Int64(Date().timeIntervalSince1970 * 1000),
editedAt: nil
)
}

查看文件

@ -36,6 +36,7 @@ public struct ImMessage: Codable, Sendable {
public let id: String
public let appId: String
public let fromUserId: String
public let fromId: String?
public let toId: String
public let chatType: ChatType
public let msgType: MsgType
@ -45,6 +46,7 @@ public struct ImMessage: Codable, Sendable {
public let groupReadCount: Int?
public let revoked: Bool?
public let createdAt: Int64
public let editedAt: Int64?
}
public struct PageResult<T: Decodable & Sendable>: Decodable, Sendable {
@ -64,11 +66,13 @@ public protocol ImEventDelegate: AnyObject {
func imClientDidDisconnect(reason: String?)
func imClientDidReceiveMessage(_ message: ImMessage)
func imClientDidReceiveGroupMessage(_ message: ImMessage)
func imClientDidReadMessage(_ message: ImMessage)
func imClientDidReceiveRevokedMessage(_ message: ImMessage)
func imClientDidError(_ error: String)
}
public extension ImEventDelegate {
func imClientDidReadMessage(_ message: ImMessage) {}
func imClientDidReceiveRevokedMessage(_ message: ImMessage) {}
}

查看文件

@ -1,4 +1,8 @@
import Foundation
import UserNotifications
#if canImport(UIKit)
import UIKit
#endif
public enum PushVendor: String {
case apns = "APNS"
@ -6,10 +10,30 @@ public enum PushVendor: String {
}
@MainActor
public final class PushSDK {
public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
public static let shared = PushSDK()
private init() {}
private override init() {
super.init()
}
@MainActor
public func requestAuthorization(options: UNAuthorizationOptions = [.alert, .badge, .sound]) async throws -> Bool {
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: options)
#if canImport(UIKit)
if granted {
await MainActor.run {
UIApplication.shared.registerForRemoteNotifications()
}
}
#endif
return granted
}
public func registerDeviceToken(_ deviceToken: Data, userId: String) async throws {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
try await registerToken(token, userId: userId)
}
public func registerToken(_ token: String, userId: String, vendor: PushVendor = .apns) async throws {
let config = XuqmSDK.shared.requireConfig()
@ -24,4 +48,20 @@ public final class PushSDK {
]
)
}
public func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .badge])
}
public func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
completionHandler()
}
}

查看文件

@ -1,9 +1,6 @@
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
public struct AppUpdateInfo: Decodable, Sendable {
@ -37,8 +34,6 @@ public final class UpdateSDK {
guard let storeURL = URL(string: url) else { return }
#if canImport(UIKit)
UIApplication.shared.open(storeURL)
#elseif canImport(AppKit)
NSWorkspace.shared.open(storeURL)
#endif
}
}

查看文件

@ -2,7 +2,99 @@ import XCTest
@testable import XuqmSDK
final class SmokeTests: XCTestCase {
func testPlaceholder() {
XCTAssertTrue(true)
func testSDKConfig() {
let config = SDKConfig(appKey: "ak_test", appSecret: "as_test")
XCTAssertEqual(config.appKey, "ak_test")
XCTAssertEqual(config.appSecret, "as_test")
XCTAssertEqual(config.appId, "ak_test")
}
func testTokenStoreSaveGetClear() {
let store = TokenStore()
store.save("test_token_123")
XCTAssertEqual(store.get(), "test_token_123")
store.clear()
XCTAssertNil(store.get())
}
func testImMessageCoding() throws {
let msg = ImMessage(
id: "msg_1",
appId: "ak_test",
fromUserId: "user_a",
fromId: "user_a",
toId: "user_b",
chatType: .single,
msgType: .text,
content: "Hello",
status: .sent,
mentionedUserIds: nil,
groupReadCount: nil,
revoked: false,
createdAt: 1714300000000,
editedAt: nil
)
let data = try JSONEncoder().encode(msg)
let decoded = try JSONDecoder().decode(ImMessage.self, from: data)
XCTAssertEqual(decoded.id, "msg_1")
XCTAssertEqual(decoded.content, "Hello")
XCTAssertEqual(decoded.status, .sent)
}
func testChatTypeAndMsgTypeRawValues() {
XCTAssertEqual(ChatType.single.rawValue, "SINGLE")
XCTAssertEqual(ChatType.group.rawValue, "GROUP")
XCTAssertEqual(MsgType.text.rawValue, "TEXT")
XCTAssertEqual(MsgType.revoked.rawValue, "REVOKED")
}
func testPageResultDecoding() throws {
let json = """
{
"content": [{"id": "1"}],
"totalElements": 10,
"totalPages": 1,
"size": 20,
"number": 0,
"numberOfElements": 1,
"first": true,
"last": true,
"empty": false
}
""".data(using: .utf8)!
struct Item: Codable, Sendable { let id: String }
let result = try JSONDecoder().decode(PageResult<Item>.self, from: json)
XCTAssertEqual(result.content.count, 1)
XCTAssertEqual(result.totalElements, 10)
XCTAssertTrue(result.first)
}
func testApiResponseDecoding() throws {
let json = """
{
"code": 200,
"status": "0",
"data": {"token": "abc123"},
"message": "success"
}
""".data(using: .utf8)!
struct TokenData: Decodable { let token: String }
let response = try JSONDecoder().decode(ApiResponse<TokenData>.self, from: json)
XCTAssertEqual(response.code, 200)
XCTAssertEqual(response.data?.token, "abc123")
}
func testEmptyResponse() throws {
let json = """
{
"code": 200,
"status": "0",
"data": null,
"message": "success"
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(ApiResponse<EmptyResponse>.self, from: json)
XCTAssertEqual(response.code, 200)
}
}