feat(ios): align with Android SDK - STOMP host header, loginWithDemo, Push/Update SDK, add editedAt/fromId, fix MESSAGE frame handling
这个提交包含在:
父节点
eaf1c00d20
当前提交
3249e4b4e5
@ -75,6 +75,7 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
appId: activeAppId ?? "",
|
appId: activeAppId ?? "",
|
||||||
fromUserId: activeUserId ?? "",
|
fromUserId: activeUserId ?? "",
|
||||||
|
fromId: activeUserId,
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
msgType: msgType,
|
msgType: msgType,
|
||||||
@ -83,7 +84,8 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
|
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
|
||||||
groupReadCount: nil,
|
groupReadCount: nil,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
createdAt: now
|
createdAt: now,
|
||||||
|
editedAt: nil
|
||||||
)
|
)
|
||||||
guard let activeAppId else {
|
guard let activeAppId else {
|
||||||
delegate?.imClientDidError("IM appId not configured")
|
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 {
|
let msg = try? JSONDecoder().decode(ImMessage.self, from: messageData) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if msg.status == .read {
|
||||||
|
delegate?.imClientDidReadMessage(msg)
|
||||||
|
}
|
||||||
if (msg.revoked ?? false) || msg.status == .revoked || msg.msgType == .revoked {
|
if (msg.revoked ?? false) || msg.status == .revoked || msg.msgType == .revoked {
|
||||||
delegate?.imClientDidReceiveRevokedMessage(msg)
|
delegate?.imClientDidReceiveRevokedMessage(msg)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if msg.chatType == .group {
|
if msg.chatType == .group {
|
||||||
delegate?.imClientDidReceiveGroupMessage(msg)
|
delegate?.imClientDidReceiveGroupMessage(msg)
|
||||||
@ -272,11 +278,15 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeToken = token
|
activeToken = token
|
||||||
sendFrame(command: "CONNECT", headers: [
|
var headers: [String: String] = [
|
||||||
"accept-version": "1.2",
|
"accept-version": "1.2",
|
||||||
"Authorization": "Bearer \(token)",
|
"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,
|
public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask,
|
||||||
@ -293,6 +303,7 @@ private extension ImMessage {
|
|||||||
id: id,
|
id: id,
|
||||||
appId: appId,
|
appId: appId,
|
||||||
fromUserId: fromUserId,
|
fromUserId: fromUserId,
|
||||||
|
fromId: fromId,
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
msgType: msgType,
|
msgType: msgType,
|
||||||
@ -301,7 +312,8 @@ private extension ImMessage {
|
|||||||
mentionedUserIds: mentionedUserIds,
|
mentionedUserIds: mentionedUserIds,
|
||||||
groupReadCount: groupReadCount,
|
groupReadCount: groupReadCount,
|
||||||
revoked: false,
|
revoked: false,
|
||||||
createdAt: createdAt
|
createdAt: createdAt,
|
||||||
|
editedAt: editedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,33 @@ public final class ImSDK {
|
|||||||
client?.connect()
|
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) {
|
public func setDelegate(_ delegate: ImEventDelegate) {
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
client?.delegate = delegate
|
client?.delegate = delegate
|
||||||
@ -282,6 +309,7 @@ public final class ImSDK {
|
|||||||
id: UUID().uuidString,
|
id: UUID().uuidString,
|
||||||
appId: XuqmSDK.shared.requireConfig().appId,
|
appId: XuqmSDK.shared.requireConfig().appId,
|
||||||
fromUserId: currentUserId ?? "",
|
fromUserId: currentUserId ?? "",
|
||||||
|
fromId: currentUserId,
|
||||||
toId: toId,
|
toId: toId,
|
||||||
chatType: chatType,
|
chatType: chatType,
|
||||||
msgType: msgType,
|
msgType: msgType,
|
||||||
@ -290,7 +318,8 @@ public final class ImSDK {
|
|||||||
mentionedUserIds: mentionedUserIds,
|
mentionedUserIds: mentionedUserIds,
|
||||||
groupReadCount: nil,
|
groupReadCount: nil,
|
||||||
revoked: false,
|
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 id: String
|
||||||
public let appId: String
|
public let appId: String
|
||||||
public let fromUserId: String
|
public let fromUserId: String
|
||||||
|
public let fromId: String?
|
||||||
public let toId: String
|
public let toId: String
|
||||||
public let chatType: ChatType
|
public let chatType: ChatType
|
||||||
public let msgType: MsgType
|
public let msgType: MsgType
|
||||||
@ -45,6 +46,7 @@ public struct ImMessage: Codable, Sendable {
|
|||||||
public let groupReadCount: Int?
|
public let groupReadCount: Int?
|
||||||
public let revoked: Bool?
|
public let revoked: Bool?
|
||||||
public let createdAt: Int64
|
public let createdAt: Int64
|
||||||
|
public let editedAt: Int64?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PageResult<T: Decodable & Sendable>: Decodable, Sendable {
|
public struct PageResult<T: Decodable & Sendable>: Decodable, Sendable {
|
||||||
@ -64,11 +66,13 @@ public protocol ImEventDelegate: AnyObject {
|
|||||||
func imClientDidDisconnect(reason: String?)
|
func imClientDidDisconnect(reason: String?)
|
||||||
func imClientDidReceiveMessage(_ message: ImMessage)
|
func imClientDidReceiveMessage(_ message: ImMessage)
|
||||||
func imClientDidReceiveGroupMessage(_ message: ImMessage)
|
func imClientDidReceiveGroupMessage(_ message: ImMessage)
|
||||||
|
func imClientDidReadMessage(_ message: ImMessage)
|
||||||
func imClientDidReceiveRevokedMessage(_ message: ImMessage)
|
func imClientDidReceiveRevokedMessage(_ message: ImMessage)
|
||||||
func imClientDidError(_ error: String)
|
func imClientDidError(_ error: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ImEventDelegate {
|
public extension ImEventDelegate {
|
||||||
|
func imClientDidReadMessage(_ message: ImMessage) {}
|
||||||
func imClientDidReceiveRevokedMessage(_ message: ImMessage) {}
|
func imClientDidReceiveRevokedMessage(_ message: ImMessage) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
#if canImport(UIKit)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
public enum PushVendor: String {
|
public enum PushVendor: String {
|
||||||
case apns = "APNS"
|
case apns = "APNS"
|
||||||
@ -6,10 +10,30 @@ public enum PushVendor: String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class PushSDK {
|
public final class PushSDK: NSObject, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
public static let shared = PushSDK()
|
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 {
|
public func registerToken(_ token: String, userId: String, vendor: PushVendor = .apns) async throws {
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
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
|
import Foundation
|
||||||
|
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
import UIKit
|
import UIKit
|
||||||
#elseif canImport(AppKit)
|
|
||||||
import AppKit
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public struct AppUpdateInfo: Decodable, Sendable {
|
public struct AppUpdateInfo: Decodable, Sendable {
|
||||||
@ -37,8 +34,6 @@ public final class UpdateSDK {
|
|||||||
guard let storeURL = URL(string: url) else { return }
|
guard let storeURL = URL(string: url) else { return }
|
||||||
#if canImport(UIKit)
|
#if canImport(UIKit)
|
||||||
UIApplication.shared.open(storeURL)
|
UIApplication.shared.open(storeURL)
|
||||||
#elseif canImport(AppKit)
|
|
||||||
NSWorkspace.shared.open(storeURL)
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,99 @@ import XCTest
|
|||||||
@testable import XuqmSDK
|
@testable import XuqmSDK
|
||||||
|
|
||||||
final class SmokeTests: XCTestCase {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户