diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..0c7ed1f --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,21 @@ +#!/bin/bash + +set -eo pipefail + +# Get the path of the JSON file +JSON_FILE_PATH="./signaling-manager/config.json" + +# Read the JSON file contents +JSON_CONTENTS=$(git show :${JSON_FILE_PATH}) + +# Extract the value of the "appId" key from the JSON +APP_ID_VALUE=$(echo "${JSON_CONTENTS}" | jq -r '.appId') + +# Check if the "appId" value is not zero +if [ -n "${APP_ID_VALUE}" ]; then + echo "Error: The 'appId' value in the JSON file must be an empty string." + exit 1 +fi + +# Exit with a success status +exit 0 diff --git a/Example-App/Example-App.xcodeproj/project.pbxproj b/Example-App/Example-App.xcodeproj/project.pbxproj index 7346b4f..8332786 100644 --- a/Example-App/Example-App.xcodeproj/project.pbxproj +++ b/Example-App/Example-App.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ F32AF8C52A8BB97B00A27ED1 /* MessageInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32AF8C22A8BB97B00A27ED1 /* MessageInputView.swift */; }; F32AF8C62A8BB97B00A27ED1 /* MessagesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32AF8C32A8BB97B00A27ED1 /* MessagesListView.swift */; }; F32AF8C92A8BB99D00A27ED1 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32AF8C82A8BB99D00A27ED1 /* View+Extensions.swift */; }; - F32AF8CC2A8BB9C400A27ED1 /* AgoraRtmKit-Swift in Frameworks */ = {isa = PBXBuildFile; productRef = F32AF8CB2A8BB9C400A27ED1 /* AgoraRtmKit-Swift */; }; F32AF8D02A8BBA1000A27ED1 /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32AF8CF2A8BBA1000A27ED1 /* GettingStartedView.swift */; }; F32AF8D32A8BBAB800A27ED1 /* ChannelInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32AF8D22A8BBAB800A27ED1 /* ChannelInputView.swift */; }; F32AF8D62A8BC19400A27ED1 /* TokenAuthenticationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32AF8D52A8BC19400A27ED1 /* TokenAuthenticationView.swift */; }; @@ -26,6 +25,7 @@ F360D0CB2AEBE81F00B0586C /* CloudProxyInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F360D0CA2AEBE81F00B0586C /* CloudProxyInputView.swift */; }; F360D0CD2AEBF23800B0586C /* DataEncryptionInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F360D0CC2AEBF23800B0586C /* DataEncryptionInputView.swift */; }; F36313292AC4728B0000F29E /* RemoteUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36313282AC4728B0000F29E /* RemoteUsersView.swift */; }; + F381E9C02AFA7E5600E8D0D5 /* AgoraRtmKit-Swift in Frameworks */ = {isa = PBXBuildFile; productRef = F381E9BF2AFA7E5600E8D0D5 /* AgoraRtmKit-Swift */; }; F3BB3B182AE92660009E00C6 /* CloudProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB3B172AE92660009E00C6 /* CloudProxyView.swift */; }; F3BB3B1A2AE92669009E00C6 /* DataEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB3B192AE92669009E00C6 /* DataEncryptionView.swift */; }; F3BB3B1C2AE92676009E00C6 /* PresenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB3B1B2AE92676009E00C6 /* PresenceView.swift */; }; @@ -71,7 +71,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F32AF8CC2A8BB9C400A27ED1 /* AgoraRtmKit-Swift in Frameworks */, + F381E9C02AFA7E5600E8D0D5 /* AgoraRtmKit-Swift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -264,7 +264,7 @@ ); name = "Example-App"; packageProductDependencies = ( - F32AF8CB2A8BB9C400A27ED1 /* AgoraRtmKit-Swift */, + F381E9BF2AFA7E5600E8D0D5 /* AgoraRtmKit-Swift */, ); productName = "Example-App"; productReference = F32AF8AB2A8BB86200A27ED1 /* Example-App.app */; @@ -295,7 +295,7 @@ ); mainGroup = F32AF8A22A8BB86200A27ED1; packageReferences = ( - F32AF8CA2A8BB9C400A27ED1 /* XCRemoteSwiftPackageReference "AgoraRtm" */, + F381E9BE2AFA7E5600E8D0D5 /* XCRemoteSwiftPackageReference "AgoraRtm_Apple" */, ); productRefGroup = F32AF8AC2A8BB86200A27ED1 /* Products */; projectDirPath = ""; @@ -565,20 +565,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - F32AF8CA2A8BB9C400A27ED1 /* XCRemoteSwiftPackageReference "AgoraRtm" */ = { + F381E9BE2AFA7E5600E8D0D5 /* XCRemoteSwiftPackageReference "AgoraRtm_Apple" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/AgoraIO-Community/AgoraRtm.git"; + repositoryURL = "https://github.com/AgoraIO/AgoraRtm_Apple.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = "2.1.6-beta"; + minimumVersion = "2.1.7-beta.1"; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - F32AF8CB2A8BB9C400A27ED1 /* AgoraRtmKit-Swift */ = { + F381E9BF2AFA7E5600E8D0D5 /* AgoraRtmKit-Swift */ = { isa = XCSwiftPackageProductDependency; - package = F32AF8CA2A8BB9C400A27ED1 /* XCRemoteSwiftPackageReference "AgoraRtm" */; + package = F381E9BE2AFA7E5600E8D0D5 /* XCRemoteSwiftPackageReference "AgoraRtm_Apple" */; productName = "AgoraRtmKit-Swift"; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Example-App/Example-App/Utilities/PassParamPages/CloudProxyInputView.swift b/Example-App/Example-App/Utilities/PassParamPages/CloudProxyInputView.swift index e82f502..4f498f2 100644 --- a/Example-App/Example-App/Utilities/PassParamPages/CloudProxyInputView.swift +++ b/Example-App/Example-App/Utilities/PassParamPages/CloudProxyInputView.swift @@ -99,6 +99,10 @@ struct CloudProxyInputView: View { }.onAppear { channelId = DocsAppConfig.shared.channel userId = DocsAppConfig.shared.uid + proxyUrl = DocsAppConfig.shared.proxyUrl + proxyPort = DocsAppConfig.shared.proxyPort + proxyAccount = DocsAppConfig.shared.proxyAccount + proxyPassword = DocsAppConfig.shared.proxyPassword } } } diff --git a/Example-App/Example-App/Utilities/PassParamPages/DataEncryptionInputView.swift b/Example-App/Example-App/Utilities/PassParamPages/DataEncryptionInputView.swift index ad8d7bc..2f638c1 100644 --- a/Example-App/Example-App/Utilities/PassParamPages/DataEncryptionInputView.swift +++ b/Example-App/Example-App/Utilities/PassParamPages/DataEncryptionInputView.swift @@ -79,6 +79,8 @@ struct DataEncryptionInputView: View { }.onAppear { channelId = DocsAppConfig.shared.channel userId = DocsAppConfig.shared.uid + encryptionKey = DocsAppConfig.shared.cipherKey + encryptionSalt = DocsAppConfig.shared.salt } } } diff --git a/authentication-workflow/TokenAuthenticationView.swift b/authentication-workflow/TokenAuthenticationView.swift index 044b219..c4f7a12 100644 --- a/authentication-workflow/TokenAuthenticationView.swift +++ b/authentication-workflow/TokenAuthenticationView.swift @@ -6,14 +6,19 @@ // import SwiftUI +import AgoraRtm extension SignalingManager { struct TokenResponse: Codable { var token: String } + var tokenUrl: String { + DocsAppConfig.shared.tokenUrl + } + func fetchToken(from urlString: String, username: String, channelName: String? = nil) async throws -> String { - guard let url = URL(string: urlString) else { + guard let url = URL(string: "\(urlString)/getToken") else { throw URLError(.badURL) } @@ -39,6 +44,24 @@ extension SignalingManager { return tokenResponse.token } + + func loginMessageChannel(tokenUrl: String, username: String) async throws { + let token = try await self.fetchToken( + from: tokenUrl, username: username + ) + + try await self.signalingEngine.login(byToken: token) + } + + func loginStreamChannel(tokenUrl: String, username: String, streamChannel: String) async throws { + let token = try await self.fetchToken( + from: tokenUrl, username: username, channelName: streamChannel + ) + + let channel = try self.signalingEngine.createStreamChannel(streamChannel) + let joinOption = RtmJoinChannelOption(token: token, features: [.presence]) + try await channel?.join(with: joinOption) + } } extension GetStartedSignalingManager { @@ -47,6 +70,13 @@ extension GetStartedSignalingManager { await self.loginAndSub(to: channel, with: token) } + + public func rtmKit(_ rtmClient: RtmClientKit, tokenPrivilegeWillExpire channel: String?) { + Task { + let token = try await self.fetchToken(from: self.tokenUrl, username: self.userId) + try await signalingEngine.renewToken(token) + } + } } // MARK: - UI @@ -96,7 +126,7 @@ struct TokenAuthenticationView: View { } func publish(message: String) async { - await self.signalingManager.publish( + await self.signalingManager.publishAndRecord( message: message, to: self.channelId ) } diff --git a/cloud-proxy/CloudProxyView.swift b/cloud-proxy/CloudProxyView.swift index 4c3ea79..5d94f4f 100644 --- a/cloud-proxy/CloudProxyView.swift +++ b/cloud-proxy/CloudProxyView.swift @@ -38,11 +38,11 @@ public class CloudProxyManager: GetStartedSignalingManager { guard reason == .settingProxyServer else { return } switch state { - case .disconnected: break - case .connecting: break - case .connected: break - case .reconnecting: break - case .failed: break + case .disconnected: print("proxy disconnected") + case .connecting: print("proxy connecting") + case .connected: print("proxy connected") + case .reconnecting: print("proxy reconnecting") + case .failed: print("proxy failed") default: break } } @@ -85,7 +85,7 @@ struct CloudProxyView: View { // MARK: - Helpers and Setup func publish(message: String) async { - await self.signalingManager.publish( + await self.signalingManager.publishAndRecord( message: message, to: self.channelId ) } diff --git a/data-encryption/DataEncryptionView.swift b/data-encryption/DataEncryptionView.swift index a2f0213..fc13e60 100644 --- a/data-encryption/DataEncryptionView.swift +++ b/data-encryption/DataEncryptionView.swift @@ -9,7 +9,8 @@ public class EncryptionSignalingManager: GetStartedSignalingManager { let config = RtmClientConfig(appId: self.appId, userId: self.userId) config.encryptionConfig = .aes128GCM( - key: encryptionKey, salt: encryptionSalt + key: encryptionKey, + salt: RtmClientConfig.encryptSaltString(salt: encryptionSalt) ) guard let eng = try? RtmClientKit( @@ -57,7 +58,7 @@ struct DataEncryptionView: View { // MARK: - Helpers and Setup func publish(message: String) async { - await self.signalingManager.publish( + await self.signalingManager.publishAndRecord( message: message, to: self.channelId ) } diff --git a/get-started-sdk/GettingStartedView.swift b/get-started-sdk/GettingStartedView.swift index 0cdf6b7..00e699f 100644 --- a/get-started-sdk/GettingStartedView.swift +++ b/get-started-sdk/GettingStartedView.swift @@ -42,16 +42,9 @@ public class GetStartedSignalingManager: SignalingManager, RtmClientDelegate { /// - Parameters: /// - message: String to be sent to the channel. UTF-8 suppported 👋. /// - channel: Channel name to publish the message to. - public func publish(message: String, to channel: String) async { - do { - try await self.signalingEngine.publish( - message: message, - to: channel - ) - } catch let err as RtmErrorInfo { - return await self.updateLabel(to: "Could not publish message: \(err.reason)") - } catch { - await self.updateLabel(to: "Unknown error: \(error.localizedDescription)") + public func publishAndRecord(message: String, to channel: String) async { + guard (try? await super.publish(message: message, to: channel)) != nil else { + return } DispatchQueue.main.async { @@ -134,7 +127,7 @@ struct GettingStartedView: View { // MARK: - Helpers and Setup func publish(message: String) async { - await self.signalingManager.publish( + await self.signalingManager.publishAndRecord( message: message, to: self.channelId ) } diff --git a/manage-connection-states/ConnectionStatesView.swift b/manage-connection-states/ConnectionStatesView.swift index a71b7bd..3536af9 100644 --- a/manage-connection-states/ConnectionStatesView.swift +++ b/manage-connection-states/ConnectionStatesView.swift @@ -12,11 +12,6 @@ public class ConnectionStatesManager: SignalingManager, RtmClientDelegate { @Published var loggedIn: Bool = false - @discardableResult - func logout() async throws -> RtmCommonResponse { - try await self.signalingEngine.logout() - } - func subscribe(_ channel: String) async throws -> RtmCommonResponse { try await self.signalingEngine.subscribe(toChannel: channel) } diff --git a/presence/PresenceView.swift b/presence/PresenceView.swift index cb93f63..929d30c 100644 --- a/presence/PresenceView.swift +++ b/presence/PresenceView.swift @@ -3,7 +3,7 @@ import SwiftUI public class PresenceSignalingManager: SignalingManager, RtmClientDelegate { - @Published public var selectedUser: RtmMetadata? + @Published public var selectedUser: RtmPresenceGetStateResponse? override func subscribe(to channel: String) async throws -> RtmCommonResponse { try await self.signalingEngine.subscribe( @@ -12,36 +12,48 @@ public class PresenceSignalingManager: SignalingManager, RtmClientDelegate { ) } - func setLocalUserMetadata(key: String, value: String) async throws { - guard let storage = self.signalingEngine.storage, - let newMetadata = storage.createMetadata() - else { return } - - newMetadata.setMetadataItem( - RtmMetadataItem(key: key, value: value) + @discardableResult + func setUserState( + in channel: String, to state: [String: String] + ) async throws -> RtmCommonResponse? { + try await self.signalingEngine.presence?.setUserState( + inChannel: .messageChannel(channel), + to: state ) - try await storage.setUserMetadata( - userId: self.userId, data: newMetadata + } + + func getState(of user: String, from channel: String) async throws -> RtmPresenceGetStateResponse? { + let presence = self.signalingEngine.presence + return try? await presence?.getState( + ofUser: user, + inChannel: .messageChannel(channel) ) } - @Published var remoteUsers: [String: [String: String]] = [:] + func getOnlineUsers(in channelName: String) async -> [String]? { + try? await signalingEngine.presence?.getOnlineUsers( + inChannel: .messageChannel(channelName), + options: RtmPresenceOptions(include: .userId) + ).users + } + + @Published var remoteUsers: [String] = [] public func rtmKit(_ rtmClient: RtmClientKit, didReceivePresenceEvent event: RtmPresenceEvent) { DispatchQueue.main.async { switch event.type { case .snapshot(let states): - self.remoteUsers = states + // states snapshot received + break case .remoteJoinChannel(let user): // remote user joined channel - if self.remoteUsers[user] == nil { - self.remoteUsers[user] = [:] - } + break case .remoteLeaveChannel(let user): // remote user left channel - self.remoteUsers.removeValue(forKey: user) + break case .remoteStateChanged(let user, let states): - self.remoteUsers.updateValue(states, forKey: user) + // remote user updated states + break default: break } } @@ -56,15 +68,16 @@ struct PresenceView: View { @ObservedObject var signalingManager: PresenceSignalingManager let channelId: String @State private var isSheetPresented: Bool = false + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() var body: some View { ZStack { if self.signalingManager.remoteUsers.isEmpty { Text("None").padding() } else { - List(signalingManager.remoteUsers.keys.sorted(), id: \.self) { key in + List(signalingManager.remoteUsers.sorted(), id: \.self) { key in Button(action: { - Task {try await self.didSelectUser(key)} + Task { try await self.didSelectUser(key) } }, label: { Text(key) }) @@ -74,23 +87,29 @@ struct PresenceView: View { } } } - }.onAppear { + }.onReceive(timer, perform: { out in + Task { + // fetch online users every 5 seconds. + // fetching directly is an alternative to checking delegate callbacks + guard let users = await self.signalingManager.getOnlineUsers(in: self.channelId) + else { return } + self.signalingManager.remoteUsers = users + } + }).onAppear { await signalingManager.loginAndSub(to: self.channelId, with: DocsAppConfig.shared.token) - try? await self.signalingManager.setLocalUserMetadata( - key: "metaId", value: UUID().uuidString - ) + _ = try? await self.signalingManager.setUserState(in: self.channelId, to: ["joinedAt": Date.now.description]) }.onDisappear { + timer.upstream.connect().cancel() try? await signalingManager.destroy() } } func didSelectUser(_ user: String) async throws { - guard let storage = self.signalingManager.signalingEngine.storage, - let data = try await storage.getMetadata(forUser: user).data + guard let states = try? await self.signalingManager.getState(of: user, from: self.channelId) else { return } DispatchQueue.main.async { - self.signalingManager.selectedUser = data + self.signalingManager.selectedUser = states self.isSheetPresented = true } } @@ -104,17 +123,18 @@ struct PresenceView: View { static var docTitle: String = "Presence" } -struct DictionaryView: View { - @State var data: RtmMetadata +fileprivate struct DictionaryView: View { + @State var data: RtmPresenceGetStateResponse var body: some View { Group { - if data.metadataItems.isEmpty { + if data.states.isEmpty { Text("No data") } else { - List(data.metadataItems, id: \.key) { item in - Text("\(item.key): \(item.value)") + List(Array(data.states.keys), id: \.self) { item in + Text("\(item): \(data.states[item] ?? "invalid")") } + Text("some data") } }.navigationTitle("User Details") } diff --git a/signaling-manager/DocsAppConfig.swift b/signaling-manager/DocsAppConfig.swift index d134af8..9a02102 100644 --- a/signaling-manager/DocsAppConfig.swift +++ b/signaling-manager/DocsAppConfig.swift @@ -59,6 +59,12 @@ public struct DocsAppConfig: Codable { var cipherKey: String /// Add Proxy Server URL var proxyUrl: String + /// Add Proxy Server Port + var proxyPort: String + /// Add Proxy Server Account + var proxyAccount: String + /// Add Proxy Server Password + var proxyPassword: String /// Add Proxy type from "none", "tcp", "udp" var proxyType: String /// Add Token Generator URL diff --git a/signaling-manager/SignalingManager.swift b/signaling-manager/SignalingManager.swift index 88fb273..a12d3fa 100644 --- a/signaling-manager/SignalingManager.swift +++ b/signaling-manager/SignalingManager.swift @@ -43,6 +43,7 @@ open class SignalingManager: NSObject, ObservableObject { @MainActor func updateLabel(to message: String) { + print("[RTM App Update]: \(message)") self.label = message } @@ -71,6 +72,63 @@ open class SignalingManager: NSObject, ObservableObject { } } + @discardableResult + func loginBasic(byToken token: String?) async -> RtmCommonResponse? { + do { + // First try logging in with the current temporary token + return try await self.signalingEngine.login(byToken: token) + } catch { + if let err = error as? RtmErrorInfo { + print("Login failed: \(err.reason)") + } else { + print("Login failed: \(error.localizedDescription)") + } + return nil + } + } + + @discardableResult + func logout() async throws -> RtmCommonResponse { + try await self.signalingEngine.logout() + } + + /// Publish a message to a message channel. + /// - Parameters: + /// - message: String to be sent to the channel. UTF-8 suppported 👋. + /// - channel: Channel name to publish the message to. + @discardableResult + open func publish(message: String, to channel: String) async throws -> RtmCommonResponse { + do { + return try await self.signalingEngine.publish( + message: message, + to: channel + ) + } catch let err as RtmErrorInfo { + await self.updateLabel(to: "Could not publish message: \(err.reason)") + throw err + } catch { + await self.updateLabel(to: "Unknown error: \(error.localizedDescription)") + throw error + } + } + + open func publishBasic( + message: String, to channel: String + ) async throws -> RtmCommonResponse { + do { + return try await self.signalingEngine.publish( + message: message, + to: channel + ) + } catch let err as RtmErrorInfo { + print("Could not publish message: \(err.reason)") + throw err + } catch { + print("Unknown error: \(error.localizedDescription)") + throw error + } + } + /// Log into Signaling with a token, and subscribe to a message channel. /// - Parameters: /// - channel: Channel name to subscribe to. @@ -95,6 +153,11 @@ open class SignalingManager: NSObject, ObservableObject { ) } + @discardableResult + func unsubscribe(from channel: String) async throws -> RtmCommonResponse { + try await self.signalingEngine.unsubscribe(fromChannel: channel) + } + func destroy() async throws { try await self.signalingEngine.logout() try self.signalingEngine.destroy() @@ -111,13 +174,14 @@ open class SignalingManager: NSObject, ObservableObject { ) async { switch error.errorCode { case .loginNoServerResources, .loginTimeout, .loginRejected, .loginAborted: - await self.updateLabel(to: "could not log in, check your app ID and token") + await self.updateLabel(to: "could not log in, check your app ID and token. error: \(error.reason)") case .channelSubscribeFailed, .channelSubscribeTimeout, .channelNotSubscribed: await self.updateLabel(to: "could not subscribe to channel") case .invalidToken: if let tryloginAgain, label == nil, let token = try? await self.fetchToken( from: DocsAppConfig.shared.tokenUrl, - username: DocsAppConfig.shared.uid + username: DocsAppConfig.shared.uid, + channelName: channel ) { await self.updateLabel(to: "fetching token") await tryloginAgain(channel, token) @@ -126,5 +190,36 @@ open class SignalingManager: NSObject, ObservableObject { await self.updateLabel(to: "failed: \(error.operation)\nreason: \(error.reason)") } } +} +/* +extension SignalingManager: RtmClientDelegate { + public func rtmKit(_ rtmClient: RtmClientKit, didReceiveMessageEvent event: RtmMessageEvent) { + // received message + } + public func rtmKit(_ rtmClient: RtmClientKit, didReceiveTopicEvent event: RtmTopicEvent) { + // received topic event + } + public func rtmKit(_ rtmClient: RtmClientKit, didReceivePresenceEvent event: RtmPresenceEvent) { + // received presence event + } + public func rtmKit(_ rtmClient: RtmClientKit, didReceiveStorageEvent event: RtmStorageEvent) { + // received storage event + } + public func rtmKit(_ rtmClient: RtmClientKit, didReceiveLockEvent event: RtmLockEvent) { + // received lock event + } + public func rtmKit(_ rtmClient: RtmClientKit, tokenPrivilegeWillExpire channel: String?) { + // current token will expire soon + } + public func rtmKit( + _ rtmClient: RtmClientKit, + channel: String, + connectionChangedToState + state: RtmClientConnectionState, + reason: RtmClientConnectionChangeReason + ) { + // connection state changed + } } +*/ diff --git a/storage/StorageView.swift b/storage/StorageView.swift index cbb53dc..3b653f5 100644 --- a/storage/StorageView.swift +++ b/storage/StorageView.swift @@ -37,8 +37,8 @@ public class StorageSignalingManager: SignalingManager, RtmClientDelegate { ) } - _ = try await storage.setUserMetadata( - userId: self.userId, data: userMetadata, + _ = try await storage.setMetadata( + forUser: self.userId, data: userMetadata, options: RtmMetadataOptions(recordTs: true, recordUserId: true) ) } @@ -47,12 +47,12 @@ public class StorageSignalingManager: SignalingManager, RtmClientDelegate { guard let localMetadata else { return } for metadataItem in localMetadata.metadataItems - where updates.keys.contains(metadataItem.key) { + where updates.keys.contains(metadataItem.key) { metadataItem.value = updates[metadataItem.key]! } - _ = try await signalingEngine.storage?.setUserMetadata( - userId: self.userId, data: localMetadata, + _ = try await signalingEngine.storage?.setMetadata( + forUser: self.userId, data: localMetadata, options: RtmMetadataOptions(recordTs: true, recordUserId: true) ) } @@ -61,8 +61,29 @@ public class StorageSignalingManager: SignalingManager, RtmClientDelegate { try await signalingEngine.storage!.getMetadata(forUser: user) } - func subscribeToMetadata(for user: String) async throws { - try await signalingEngine.storage?.subscribeToMetadata(forUser: user) + func subscribeToMetadata(for user: String) async { + do { + try await signalingEngine.storage?.subscribeToMetadata(forUser: user) + } catch { + // could not subscribe to metadata + } + } + + func updateMetadata(for user: String, data: [String: String]) async { + guard let storage = signalingEngine.storage, + let metadata = storage.createMetadata() + else { return } + + for item in data { + metadata.setMetadataItem( + RtmMetadataItem(key: item.key, value: item.value) + ) + } + do { + try await signalingEngine.storage?.updateMetadata(forUser: user, data: metadata) + } catch { + // could not update metadata + } } func setMetadata( @@ -94,8 +115,21 @@ public class StorageSignalingManager: SignalingManager, RtmClientDelegate { try await signalingEngine.lock?.releaseLock( named: lock, fromChannel: .messageChannel(channel) ) - await self.updateLabel(to: "success") } + await self.updateLabel(to: "success") + } + + func setMetadata(forUser user: String, items: [String: String]) { + guard let storage = signalingEngine.storage, + let metadata = storage.createMetadata() + else { return } + + for item in items { + metadata.setMetadataItem( + RtmMetadataItem(key: item.key, value: item.value) + ) + } + storage.setMetadata(forUser: user, data: metadata) } func getMetadata(forChannel channel: String) async throws -> RtmGetMetadataResponse { @@ -112,8 +146,8 @@ public class StorageSignalingManager: SignalingManager, RtmClientDelegate { removeMetadata.setMetadataItem(.init(key: key, value: "")) } - _ = try await signalingEngine.storage?.removeUserMetadata( - userId: self.userId, data: removeMetadata + _ = try await signalingEngine.storage?.removeMetadata( + forUser: self.userId, data: removeMetadata ) } public func rtmKit(_ rtmClient: RtmClientKit, didReceiveStorageEvent event: RtmStorageEvent) { @@ -127,7 +161,42 @@ public class StorageSignalingManager: SignalingManager, RtmClientDelegate { default: break } } +} +extension StorageSignalingManager { + func setLock(lockName: String, channel: String, ttl: Int32) async throws { + try await signalingEngine.lock?.setLock( + named: lockName, + forChannel: .messageChannel(channel), + ttl: ttl + ) + } + func acquireLock(lockName: String, channel: String, retry: Bool) async -> RtmCommonResponse? { + try? await signalingEngine.lock?.acquireLock( + named: lockName, + fromChannel: .messageChannel(channel), + retry: retry + ) + } + + func releaseLock(lockName: String, channel: String, retry: Bool) { + signalingEngine.lock?.releaseLock( + named: lockName, fromChannel: .messageChannel(channel) + ) + } + + func removeLock(lockName: String, channel: String) { + signalingEngine.lock?.removeLock( + named: lockName, fromChannel: .messageChannel(channel) + ) + } + + func getLocks(inChannel channel: String) async throws -> RtmGetLocksResponse? { + try await signalingEngine.lock?.getLocks(forChannel: .messageChannel(channel)) + } +} + +extension StorageSignalingManager { // MARK: Handle Errors /// Handle different error cases, and fetch a new token if appropriate. /// - Parameters: @@ -216,6 +285,23 @@ struct StorageView: View { static var docTitle: String = "Store channel and user data" } +fileprivate struct DictionaryView: View { + @State var data: RtmMetadata + + var body: some View { + Group { + if data.metadataItems.isEmpty { + Text("No data") + } else { + List(data.metadataItems, id: \.key) { item in + Text("\(item.key): \(item.value)") + } + } + }.navigationTitle("User Details") + } +} + + // MARK: - Previews struct StorageView_Previews: PreviewProvider { diff --git a/stream-channels/StreamChannelsView.swift b/stream-channels/StreamChannelsView.swift index 2240819..e503e6d 100644 --- a/stream-channels/StreamChannelsView.swift +++ b/stream-channels/StreamChannelsView.swift @@ -17,50 +17,33 @@ public class StreamChannelSignalingManager: SignalingManager, RtmClientDelegate internal var streamChannel: RtmStreamChannel? - /// Log into Signaling with a token, and subscribe to a message channel. - /// - Parameters: - /// - channel: Channel name to subscribe to. - /// - token: token to be used for login authentication. - public func loginAndJoin(streamChannel channelName: String, with token: String?) async { + func joinChannel(_ channel: String, with token: String?) async throws -> RtmStreamChannel { do { - try await self.login(byToken: token) - - // Get stream channel token - var streamChannelToken: String? - if !DocsAppConfig.shared.tokenUrl.isEmpty { - streamChannelToken = try? await self.fetchToken( - from: DocsAppConfig.shared.tokenUrl, - username: DocsAppConfig.shared.uid, - channelName: channelName - ) - } - // Create stream channel guard let streamChannel = try signalingEngine - .createStreamChannel(channelName) else { - return await self.updateLabel(to: "creating stream channel failed") + .createStreamChannel(channel) else { + fatalError("could not create channel") } // Join Stream Channel - _ = try await streamChannel.join(with: RtmJoinChannelOption( - token: streamChannelToken, features: [.presence] + try await streamChannel.join(with: RtmJoinChannelOption( + token: token, features: [.presence] )) - self.streamChannel = streamChannel - - await self.updateLabel(to: "success") - } catch let err as RtmErrorInfo { - await self.handleLoginSubError(error: err, channel: channelName) + return streamChannel } catch { - await self.updateLabel(to: "other error occurred: \(error.localizedDescription)") + // could not join channel + throw error } } + + func leaveChannel() async throws { + try await self.streamChannel?.leave() + } func joinTopic(named topic: String) async throws { try await self.streamChannel?.joinTopic( topic, with: RtmJoinTopicOption(qos: .ordered) ) - print("self.streamChannel") - print(self.streamChannel) } func subTopic(named topic: String) async throws { @@ -78,15 +61,50 @@ public class StreamChannelSignalingManager: SignalingManager, RtmClientDelegate } } + @discardableResult + func publish( + message: String, in channel: RtmStreamChannel, topic: String + ) async throws -> RtmCommonResponse { + try await channel.publishTopicMessage( + message: message, inTopic: topic, with: nil + ) + } + + /// Log into Signaling with a token, and subscribe to a message channel. + /// - Parameters: + /// - channel: Channel name to subscribe to. + /// - token: token to be used for login authentication. + public func loginAndJoin(streamChannel channelName: String, with token: String?) async { + do { + try await self.login(byToken: token) + + // Get stream channel token + var streamChannelToken: String? + if !DocsAppConfig.shared.tokenUrl.isEmpty { + streamChannelToken = try? await self.fetchToken( + from: DocsAppConfig.shared.tokenUrl, + username: DocsAppConfig.shared.uid, + channelName: channelName + ) + } + + self.streamChannel = try await self.joinChannel(channelName, with: streamChannelToken) + + await self.updateLabel(to: "success") + } catch let err as RtmErrorInfo { + await self.handleLoginSubError(error: err, channel: channelName) + } catch { + await self.updateLabel(to: "other error occurred: \(error.localizedDescription)") + } + } + /// Publish a message to a message channel. /// - Parameters: /// - message: String to be sent to the channel. UTF-8 suppported 👋. /// - channel: Channel name to publish the message to. public func publish(message: String, in channel: RtmStreamChannel, to topic: String) async { do { - _ = try await self.streamChannel?.publishTopicMessage( - message: message, inTopic: topic, with: nil - ) + try await self.publish(message: message, in: channel, topic: topic) } catch let err as RtmErrorInfo { return await self.updateLabel(to: "Could not publish message: \(err.reason)") } catch { @@ -199,6 +217,7 @@ struct StreamChannelsView: View { await signalingManager.loginAndJoin(streamChannel: self.channelId, with: DocsAppConfig.shared.token ) }.onDisappear { + try? await self.signalingManager.leaveChannel() try? await signalingManager.destroy() }.sheet(isPresented: $showingAddTopicView) { // 3. Present the add topic view