diff --git a/ios/RocketChat Watch App/ContentView.swift b/ios/RocketChat Watch App/ContentView.swift index b000a7e46..8ea2ef6fe 100644 --- a/ios/RocketChat Watch App/ContentView.swift +++ b/ios/RocketChat Watch App/ContentView.swift @@ -1,6 +1,24 @@ import SwiftUI +final class ContentViewModel: ObservableObject { + private let connection: Connection + + init(connection: Connection) { + self.connection = connection + } + + func onAppear() { + connection.sendMessage { result in + print(result) + } + } +} + struct ContentView: View { + @StateObject var viewModel = ContentViewModel( + connection: WatchConnection() + ) + var body: some View { VStack { Image(systemName: "globe") @@ -9,6 +27,9 @@ struct ContentView: View { Text("Hello, world!") } .padding() + .onAppear { + viewModel.onAppear() + } } } diff --git a/ios/RocketChat Watch App/WatchConnection.swift b/ios/RocketChat Watch App/WatchConnection.swift new file mode 100644 index 000000000..1777917eb --- /dev/null +++ b/ios/RocketChat Watch App/WatchConnection.swift @@ -0,0 +1,68 @@ +import Foundation +import WatchConnectivity + +enum ConnectionError: Error { + case needsUnlock + case decoding(Error) +} + +protocol Connection { + func sendMessage(completionHandler: @escaping (Result) -> Void) +} + +final class WatchConnection: NSObject { + private let session: WCSession + + init(session: WCSession = .default) { + self.session = session + super.init() + session.delegate = self + session.activate() + } + + private func scheduledSendMessage(completionHandler: @escaping (Result) -> Void) { + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in + self?.sendMessage(completionHandler: completionHandler) + } + } +} + +// MARK: - WCSessionDelegate + +extension WatchConnection: WCSessionDelegate { + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + + } +} + +// MARK: - Connection + +extension WatchConnection: Connection { + func sendMessage(completionHandler: @escaping (Result) -> Void) { + guard session.activationState == .activated else { + scheduledSendMessage(completionHandler: completionHandler) + return + } + + guard !session.iOSDeviceNeedsUnlockAfterRebootForReachability else { + completionHandler(.failure(.needsUnlock)) + return + } + + guard session.isReachable else { + scheduledSendMessage(completionHandler: completionHandler) + return + } + + session.sendMessage([:]) { dictionary in + do { + let data = try JSONSerialization.data(withJSONObject: dictionary) + let message = try JSONDecoder().decode(WatchMessage.self, from: data) + + completionHandler(.success(message)) + } catch { + completionHandler(.failure(.decoding(error))) + } + } + } +} diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 40415921a..905edd950 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -79,6 +79,28 @@ 1ED038952B507B4D00C007D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1ED038942B507B4D00C007D4 /* Assets.xcassets */; }; 1ED038982B507B4D00C007D4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1ED038972B507B4D00C007D4 /* Preview Assets.xcassets */; }; 1ED0389B2B507B4D00C007D4 /* Rocket.Chat.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 1ED0388E2B507B4B00C007D4 /* Rocket.Chat.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 1ED038A12B508FE700C007D4 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A02B508FE700C007D4 /* FileManager+Extensions.swift */; }; + 1ED038A22B508FE700C007D4 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A02B508FE700C007D4 /* FileManager+Extensions.swift */; }; + 1ED038A32B508FE700C007D4 /* FileManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A02B508FE700C007D4 /* FileManager+Extensions.swift */; }; + 1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A42B50900800C007D4 /* Bundle+Extensions.swift */; }; + 1ED038A62B50900800C007D4 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A42B50900800C007D4 /* Bundle+Extensions.swift */; }; + 1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A42B50900800C007D4 /* Bundle+Extensions.swift */; }; + 1ED038A92B5090AD00C007D4 /* MMKV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A82B5090AD00C007D4 /* MMKV.swift */; }; + 1ED038AA2B5090AD00C007D4 /* MMKV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A82B5090AD00C007D4 /* MMKV.swift */; }; + 1ED038AB2B5090AD00C007D4 /* MMKV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038A82B5090AD00C007D4 /* MMKV.swift */; }; + 1ED038AD2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038AC2B50927B00C007D4 /* WatermelonDB+Extensions.swift */; }; + 1ED038AE2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038AC2B50927B00C007D4 /* WatermelonDB+Extensions.swift */; }; + 1ED038AF2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038AC2B50927B00C007D4 /* WatermelonDB+Extensions.swift */; }; + 1ED038BA2B50A1B800C007D4 /* WatchConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038B92B50A1B800C007D4 /* WatchConnection.swift */; }; + 1ED038BB2B50A1B800C007D4 /* WatchConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038B92B50A1B800C007D4 /* WatchConnection.swift */; }; + 1ED038BE2B50A1D400C007D4 /* DBServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038BD2B50A1D400C007D4 /* DBServer.swift */; }; + 1ED038BF2B50A1D400C007D4 /* DBServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038BD2B50A1D400C007D4 /* DBServer.swift */; }; + 1ED038C12B50A1E400C007D4 /* DBUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C02B50A1E400C007D4 /* DBUser.swift */; }; + 1ED038C22B50A1E400C007D4 /* DBUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C02B50A1E400C007D4 /* DBUser.swift */; }; + 1ED038C42B50A1F500C007D4 /* WatchMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */; }; + 1ED038C52B50A1F500C007D4 /* WatchMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */; }; + 1ED038C62B50A21800C007D4 /* WatchMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */; }; + 1ED038CA2B50A58400C007D4 /* WatchConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038C92B50A58400C007D4 /* WatchConnection.swift */; }; 1ED59D4C22CBA77D00C54289 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1ED59D4B22CBA77D00C54289 /* GoogleService-Info.plist */; }; 1EF5FBD1250C109E00614FEA /* Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5FBD0250C109E00614FEA /* Encryption.swift */; }; 1EFEB5982493B6640072EDC0 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFEB5972493B6640072EDC0 /* NotificationService.swift */; }; @@ -287,6 +309,15 @@ 1ED038922B507B4C00C007D4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 1ED038942B507B4D00C007D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1ED038972B507B4D00C007D4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 1ED038A02B508FE700C007D4 /* FileManager+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extensions.swift"; sourceTree = ""; }; + 1ED038A42B50900800C007D4 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; + 1ED038A82B5090AD00C007D4 /* MMKV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MMKV.swift; sourceTree = ""; }; + 1ED038AC2B50927B00C007D4 /* WatermelonDB+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WatermelonDB+Extensions.swift"; sourceTree = ""; }; + 1ED038B92B50A1B800C007D4 /* WatchConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnection.swift; sourceTree = ""; }; + 1ED038BD2B50A1D400C007D4 /* DBServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBServer.swift; sourceTree = ""; }; + 1ED038C02B50A1E400C007D4 /* DBUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBUser.swift; sourceTree = ""; }; + 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchMessage.swift; sourceTree = ""; }; + 1ED038C92B50A58400C007D4 /* WatchConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnection.swift; sourceTree = ""; }; 1ED59D4B22CBA77D00C54289 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; 1EF5FBD0250C109E00614FEA /* Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encryption.swift; sourceTree = ""; }; 1EFEB5952493B6640072EDC0 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -462,6 +493,7 @@ 1E1C2F7F250FCB69005DCE7D /* Database.swift */, 1E470E822513A71E00E3DD1D /* RocketChat.swift */, 1EB8EF712510F1EE00F352B7 /* Storage.swift */, + 1ED038A82B5090AD00C007D4 /* MMKV.swift */, ); path = RocketChat; sourceTree = ""; @@ -473,6 +505,9 @@ 1E598AE32515057D002BDFBD /* Date+Extensions.swift */, 1E598AE625150660002BDFBD /* Data+Extensions.swift */, 1E67380324DC529B0009E081 /* String+Extensions.swift */, + 1ED038A02B508FE700C007D4 /* FileManager+Extensions.swift */, + 1ED038A42B50900800C007D4 /* Bundle+Extensions.swift */, + 1ED038AC2B50927B00C007D4 /* WatermelonDB+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -512,6 +547,7 @@ 1ED038922B507B4C00C007D4 /* ContentView.swift */, 1ED038942B507B4D00C007D4 /* Assets.xcassets */, 1ED038962B507B4D00C007D4 /* Preview Content */, + 1ED038C92B50A58400C007D4 /* WatchConnection.swift */, ); path = "RocketChat Watch App"; sourceTree = ""; @@ -524,6 +560,25 @@ path = "Preview Content"; sourceTree = ""; }; + 1ED038B82B50A1A500C007D4 /* Watch */ = { + isa = PBXGroup; + children = ( + 1ED038BC2B50A1C700C007D4 /* Database */, + 1ED038B92B50A1B800C007D4 /* WatchConnection.swift */, + 1ED038C32B50A1F500C007D4 /* WatchMessage.swift */, + ); + path = Watch; + sourceTree = ""; + }; + 1ED038BC2B50A1C700C007D4 /* Database */ = { + isa = PBXGroup; + children = ( + 1ED038BD2B50A1D400C007D4 /* DBServer.swift */, + 1ED038C02B50A1E400C007D4 /* DBUser.swift */, + ); + path = Database; + sourceTree = ""; + }; 1EFEB5962493B6640072EDC0 /* NotificationService */ = { isa = PBXGroup; children = ( @@ -579,6 +634,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 1ED038B82B50A1A500C007D4 /* Watch */, 1E76CBC425152A7F0067298C /* Shared */, 1E068CFB24FD2DAF00A0FFC1 /* AppGroup */, 13B07FAE1A68108700A75B9A /* RocketChatRN */, @@ -1438,6 +1494,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1ED038A92B5090AD00C007D4 /* MMKV.swift in Sources */, 1E76CBCB25152C250067298C /* Sender.swift in Sources */, 1E76CBD825152C870067298C /* Request.swift in Sources */, 1ED00BB12513E04400A1331F /* ReplyNotification.swift in Sources */, @@ -1454,11 +1511,18 @@ 1E76CBD225152C730067298C /* Data+Extensions.swift in Sources */, 1E76CBD125152C710067298C /* Date+Extensions.swift in Sources */, 1E76CBD425152C790067298C /* Database.swift in Sources */, + 1ED038AD2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, + 1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1E76CBC325152A460067298C /* String+Extensions.swift in Sources */, + 1ED038BA2B50A1B800C007D4 /* WatchConnection.swift in Sources */, + 1ED038A12B508FE700C007D4 /* FileManager+Extensions.swift in Sources */, 1E76CBCA25152C220067298C /* Notification.swift in Sources */, + 1ED038C12B50A1E400C007D4 /* DBUser.swift in Sources */, 1E76CBD525152C7F0067298C /* API.swift in Sources */, 1E76CBD625152C820067298C /* Response.swift in Sources */, + 1ED038BE2B50A1D400C007D4 /* DBServer.swift in Sources */, 1E068D0124FD2E0500A0FFC1 /* AppGroup.m in Sources */, + 1ED038C42B50A1F500C007D4 /* WatchMessage.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, 1E76CBD025152C6E0067298C /* URL+Extensions.swift in Sources */, 1E068CFE24FD2DC700A0FFC1 /* AppGroup.swift in Sources */, @@ -1486,6 +1550,8 @@ buildActionMask = 2147483647; files = ( 1ED038932B507B4C00C007D4 /* ContentView.swift in Sources */, + 1ED038CA2B50A58400C007D4 /* WatchConnection.swift in Sources */, + 1ED038C62B50A21800C007D4 /* WatchMessage.swift in Sources */, 1ED038912B507B4C00C007D4 /* RocketChatApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1497,6 +1563,8 @@ 1E51D965251263D600DC95DE /* NotificationType.swift in Sources */, 1EF5FBD1250C109E00614FEA /* Encryption.swift in Sources */, 1E598AE42515057D002BDFBD /* Date+Extensions.swift in Sources */, + 1ED038A22B508FE700C007D4 /* FileManager+Extensions.swift in Sources */, + 1ED038AA2B5090AD00C007D4 /* MMKV.swift in Sources */, 1E01C81C2511208400FEF824 /* URL+Extensions.swift in Sources */, 1E470E832513A71E00E3DD1D /* RocketChat.swift in Sources */, 1E2F615D25128FA300871711 /* Response.swift in Sources */, @@ -1505,6 +1573,8 @@ 1E0426E7251A54B4008F022C /* RoomType.swift in Sources */, 1E1C2F80250FCB69005DCE7D /* Database.swift in Sources */, 1E67380424DC529B0009E081 /* String+Extensions.swift in Sources */, + 1ED038AE2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, + 1ED038A62B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1E01C8292511304100FEF824 /* Sender.swift in Sources */, 1E51D962251263CD00DC95DE /* MessageType.swift in Sources */, 1E01C82B2511335A00FEF824 /* Message.swift in Sources */, @@ -1526,6 +1596,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1ED038AB2B5090AD00C007D4 /* MMKV.swift in Sources */, 7AAB3E15257E6A6E00707CF6 /* Sender.swift in Sources */, 7AAB3E16257E6A6E00707CF6 /* Request.swift in Sources */, 7AAB3E17257E6A6E00707CF6 /* ReplyNotification.swift in Sources */, @@ -1542,11 +1613,18 @@ 7AAB3E23257E6A6E00707CF6 /* Data+Extensions.swift in Sources */, 7AAB3E24257E6A6E00707CF6 /* Date+Extensions.swift in Sources */, 7AAB3E25257E6A6E00707CF6 /* Database.swift in Sources */, + 1ED038AF2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, + 1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 7AAB3E26257E6A6E00707CF6 /* String+Extensions.swift in Sources */, + 1ED038BB2B50A1B800C007D4 /* WatchConnection.swift in Sources */, + 1ED038A32B508FE700C007D4 /* FileManager+Extensions.swift in Sources */, 7AAB3E27257E6A6E00707CF6 /* Notification.swift in Sources */, + 1ED038C22B50A1E400C007D4 /* DBUser.swift in Sources */, 7AAB3E28257E6A6E00707CF6 /* API.swift in Sources */, 7AAB3E29257E6A6E00707CF6 /* Response.swift in Sources */, + 1ED038BF2B50A1D400C007D4 /* DBServer.swift in Sources */, 7AAB3E2A257E6A6E00707CF6 /* AppGroup.m in Sources */, + 1ED038C52B50A1F500C007D4 /* WatchMessage.swift in Sources */, 7AAB3E2B257E6A6E00707CF6 /* main.m in Sources */, 7AAB3E2C257E6A6E00707CF6 /* URL+Extensions.swift in Sources */, 7AAB3E2D257E6A6E00707CF6 /* AppGroup.swift in Sources */, diff --git a/ios/RocketChatRN.xcodeproj/xcshareddata/xcschemes/RocketChat Watch.xcscheme b/ios/RocketChatRN.xcodeproj/xcshareddata/xcschemes/RocketChat Watch.xcscheme index 60e1d5a86..5b7603af6 100644 --- a/ios/RocketChatRN.xcodeproj/xcshareddata/xcschemes/RocketChat Watch.xcscheme +++ b/ios/RocketChatRN.xcodeproj/xcshareddata/xcschemes/RocketChat Watch.xcscheme @@ -15,7 +15,7 @@ @@ -58,7 +58,7 @@ @@ -75,7 +75,7 @@ diff --git a/ios/RocketChatRN/AppDelegate.h b/ios/RocketChatRN/AppDelegate.h index 8d328571a..7ba85e39b 100644 --- a/ios/RocketChatRN/AppDelegate.h +++ b/ios/RocketChatRN/AppDelegate.h @@ -10,6 +10,7 @@ #import #import #import +#import // https://github.com/expo/expo/issues/17705#issuecomment-1196251146 #import "ExpoModulesCore-Swift.h" #import "RocketChatRN-Swift.h" @@ -17,5 +18,6 @@ @interface AppDelegate : EXAppDelegateWrapper @property (nonatomic, strong) UIWindow *window; +@property (nonatomic, strong) WatchConnection *watchConnection; @end diff --git a/ios/RocketChatRN/AppDelegate.mm b/ios/RocketChatRN/AppDelegate.mm index 4418704fe..47997f78e 100644 --- a/ios/RocketChatRN/AppDelegate.mm +++ b/ios/RocketChatRN/AppDelegate.mm @@ -69,6 +69,8 @@ [MMKV initializeMMKV:nil groupDir:groupDir logLevel:MMKVLogInfo]; [RNBootSplash initWithStoryboard:@"LaunchScreen" rootView:rootView]; + + self.watchConnection = [[WatchConnection alloc] initWithSession:[WCSession defaultSession]]; return YES; } diff --git a/ios/Shared/Extensions/Bundle+Extensions.swift b/ios/Shared/Extensions/Bundle+Extensions.swift new file mode 100644 index 000000000..8873e7b2c --- /dev/null +++ b/ios/Shared/Extensions/Bundle+Extensions.swift @@ -0,0 +1,15 @@ +import Foundation + +extension Bundle { + func bool(forKey key: String) -> Bool { + object(forInfoDictionaryKey: key) as? Bool ?? false + } + + func string(forKey key: String) -> String { + guard let string = object(forInfoDictionaryKey: key) as? String else { + fatalError("Could not locate string for key \(key).") + } + + return string + } +} diff --git a/ios/Shared/Extensions/FileManager+Extensions.swift b/ios/Shared/Extensions/FileManager+Extensions.swift new file mode 100644 index 000000000..92f60c92c --- /dev/null +++ b/ios/Shared/Extensions/FileManager+Extensions.swift @@ -0,0 +1,13 @@ +import Foundation + +extension FileManager { + func groupDir() -> String { + let applicationGroupIdentifier = Bundle.main.string(forKey: "AppGroup") + + guard let path = containerURL(forSecurityApplicationGroupIdentifier: applicationGroupIdentifier)?.path else { + return "" + } + + return path + } +} diff --git a/ios/Shared/Extensions/WatermelonDB+Extensions.swift b/ios/Shared/Extensions/WatermelonDB+Extensions.swift new file mode 100644 index 000000000..47d5ebd6e --- /dev/null +++ b/ios/Shared/Extensions/WatermelonDB+Extensions.swift @@ -0,0 +1,33 @@ +import WatermelonDB + +extension WatermelonDB.Database { + convenience init(name: String) { + let isOfficial = Bundle.main.bool(forKey: "IS_OFFICIAL") + let groupDir = FileManager.default.groupDir() + let path = "\(groupDir)/\(name)\(isOfficial ? "" : "-experimental").db" + + self.init(path: path) + } + + func query(raw: SQL, _ args: QueryArgs = []) -> [T] { + guard let results = try? queryRaw(raw, args) else { + return [] + } + + return results.compactMap { result in + guard let dictionary = result.resultDictionary else { + return nil + } + + guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { + return nil + } + + guard let item = try? JSONDecoder().decode(T.self, from: data) else { + return nil + } + + return item + } + } +} diff --git a/ios/Shared/RocketChat/Database.swift b/ios/Shared/RocketChat/Database.swift index 7e7928506..dc761f963 100644 --- a/ios/Shared/RocketChat/Database.swift +++ b/ios/Shared/RocketChat/Database.swift @@ -10,38 +10,21 @@ import Foundation import WatermelonDB final class Database { - private final var database: WatermelonDB.Database? = nil - - private var directory: String? { - if let suiteName = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String { - if let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) { - return directory.path - } - } - - return nil - } + private let database: WatermelonDB.Database init(server: String) { - if let url = URL(string: server) { - if let domain = url.domain, let directory = directory { - let isOfficial = Bundle.main.object(forInfoDictionaryKey: "IS_OFFICIAL") as? Bool ?? false - self.database = WatermelonDB.Database(path: "\(directory)/\(domain)\(isOfficial ? "" : "-experimental").db") - } - } + database = .init(name: server) } func readRoomEncryptionKey(rid: String) -> String? { - if let database = database { - if let results = try? database.queryRaw("select * from subscriptions where id == ? limit 1", [rid]) { - guard let record = results.next() else { - return nil - } - - if let room = record.resultDictionary as? [String: Any] { - if let e2eKey = room["e2e_key"] as? String { - return e2eKey - } + if let results = try? database.queryRaw("select * from subscriptions where id == ? limit 1", [rid]) { + guard let record = results.next() else { + return nil + } + + if let room = record.resultDictionary as? [String: Any] { + if let e2eKey = room["e2e_key"] as? String { + return e2eKey } } } @@ -50,16 +33,14 @@ final class Database { } func readRoomEncrypted(rid: String) -> Bool { - if let database = database { - if let results = try? database.queryRaw("select * from subscriptions where id == ? limit 1", [rid]) { - guard let record = results.next() else { - return false - } - - if let room = record.resultDictionary as? [String: Any] { - if let encrypted = room["encrypted"] as? Bool { - return encrypted - } + if let results = try? database.queryRaw("select * from subscriptions where id == ? limit 1", [rid]) { + guard let record = results.next() else { + return false + } + + if let room = record.resultDictionary as? [String: Any] { + if let encrypted = room["encrypted"] as? Bool { + return encrypted } } } diff --git a/ios/Shared/RocketChat/MMKV.swift b/ios/Shared/RocketChat/MMKV.swift new file mode 100644 index 000000000..05a8cafeb --- /dev/null +++ b/ios/Shared/RocketChat/MMKV.swift @@ -0,0 +1,40 @@ +import Foundation + +extension MMKV { + static func build() -> MMKV { + let password = SecureStorage().getSecureKey("com.MMKV.default".toHex()) + let groupDir = FileManager.default.groupDir() + + MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none) + + guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else { + fatalError("Could not initialize MMKV instance.") + } + + return mmkv + } + + func userToken(for userId: String) -> String { + guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else { + fatalError("userToken is nil for userId \(userId)") + } + + return userToken + } + + func userId(for server: String) -> String { + guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else { + fatalError("userId is nil for server \(server)") + } + + return userId + } + + func privateKey(for server: String) -> String { + guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else { + fatalError("privateKey is nil for server \(server)") + } + + return privateKey + } +} diff --git a/ios/Shared/RocketChat/Storage.swift b/ios/Shared/RocketChat/Storage.swift index 7f2496897..a2e8a4e03 100644 --- a/ios/Shared/RocketChat/Storage.swift +++ b/ios/Shared/RocketChat/Storage.swift @@ -1,11 +1,3 @@ -// -// Storage.swift -// NotificationService -// -// Created by Djorkaeff Alexandre Vilela Pereira on 9/15/20. -// Copyright © 2020 Rocket.Chat. All rights reserved. -// - import Foundation struct Credentials { @@ -13,48 +5,19 @@ struct Credentials { let userToken: String } -class Storage { +final class Storage { static let shared = Storage() - final var mmkv: MMKV? = nil + private let mmkv = MMKV.build() - init() { - let mmapID = "default" - let instanceID = "com.MMKV.\(mmapID)" - let secureStorage = SecureStorage() - - // get mmkv instance password from keychain - var key: Data? - if let password: String = secureStorage.getSecureKey(instanceID.toHex()) { - key = password.data(using: .utf8) - } - - guard let cryptKey = key else { - return - } - - // Get App Group directory - let suiteName = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String - guard let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) else { - return - } - - // Set App Group dir - MMKV.initialize(rootDir: nil, groupDir: directory.path, logLevel: MMKVLogLevel.none) - self.mmkv = MMKV(mmapID: mmapID, cryptKey: cryptKey, mode: MMKVMode.multiProcess) + func getCredentials(server: String) -> Credentials { + let userId = mmkv.userId(for: server) + let userToken = mmkv.userToken(for: userId) + + return .init(userId: userId, userToken: userToken) } - func getCredentials(server: String) -> Credentials? { - if let userId = self.mmkv?.string(forKey: "reactnativemeteor_usertoken-\(server)") { - if let userToken = self.mmkv?.string(forKey: "reactnativemeteor_usertoken-\(userId)") { - return Credentials(userId: userId, userToken: userToken) - } - } - - return nil - } - - func getPrivateKey(server: String) -> String? { - return self.mmkv?.string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") + func getPrivateKey(server: String) -> String { + mmkv.privateKey(for: server) } } diff --git a/ios/Watch/Database/DBServer.swift b/ios/Watch/Database/DBServer.swift new file mode 100644 index 000000000..b218841c8 --- /dev/null +++ b/ios/Watch/Database/DBServer.swift @@ -0,0 +1,19 @@ +import Foundation + +struct DBServer: Codable { + let url: URL + let name: String + let useRealName: Int + let iconURL: URL + + var identifier: String { + url.absoluteString + } + + enum CodingKeys: String, CodingKey { + case url = "id" + case name + case useRealName = "use_real_name" + case iconURL = "icon_url" + } +} diff --git a/ios/Watch/Database/DBUser.swift b/ios/Watch/Database/DBUser.swift new file mode 100644 index 000000000..4641ffebe --- /dev/null +++ b/ios/Watch/Database/DBUser.swift @@ -0,0 +1,6 @@ +import Foundation + +struct DBUser: Codable { + let name: String + let username: String +} diff --git a/ios/Watch/WatchConnection.swift b/ios/Watch/WatchConnection.swift new file mode 100644 index 000000000..6d747852b --- /dev/null +++ b/ios/Watch/WatchConnection.swift @@ -0,0 +1,82 @@ +import Foundation +import WatermelonDB +import WatchConnectivity + +@objc +final class WatchConnection: NSObject { + private let database = WatermelonDB.Database(name: "default") + private let mmkv = MMKV.build() + private let session: WCSession + + @objc init(session: WCSession) { + self.session = session + super.init() + + if WCSession.isSupported() { + session.delegate = self + session.activate() + } + } + + private func getMessage() -> WatchMessage { + let serversQuery = database.query(raw: "select * from servers") as [DBServer] + + let servers = serversQuery.compactMap { item -> WatchMessage.Server? in + let userId = mmkv.userId(for: item.identifier) + let userToken = mmkv.userToken(for: userId) + + let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser] + + guard let user = usersQuery.first else { + return nil + } + + return WatchMessage.Server( + url: item.url, + name: item.name, + iconURL: item.iconURL, + useRealName: item.useRealName == 1 ? true : false, + loggedUser: .init( + id: userId, + token: userToken, + name: user.name, + username: user.username + ) + ) + } + + return WatchMessage(servers: servers) + } + + private func encodedMessage() -> [String: Any] { + do { + let data = try JSONEncoder().encode(getMessage()) + + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + fatalError("Could not serialize message: \(getMessage())") + } + + return dictionary + } catch { + fatalError("Could not encode message: \(getMessage())") + } + } +} + +extension WatchConnection: WCSessionDelegate { + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + + } + + func sessionDidBecomeInactive(_ session: WCSession) { + session.activate() + } + + func sessionDidDeactivate(_ session: WCSession) { + session.activate() + } + + func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { + replyHandler(encodedMessage()) + } +} diff --git a/ios/Watch/WatchMessage.swift b/ios/Watch/WatchMessage.swift new file mode 100644 index 000000000..534eb606e --- /dev/null +++ b/ios/Watch/WatchMessage.swift @@ -0,0 +1,20 @@ +import Foundation + +struct WatchMessage: Codable { + let servers: [Server] + + struct Server: Codable { + let url: URL + let name: String + let iconURL: URL + let useRealName: Bool + let loggedUser: LoggedUser + + struct LoggedUser: Codable { + let id: String + let token: String + let name: String + let username: String + } + } +}