From 1cee1e08d5fe92cb78ca9a43375244c326f5ce7b Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Sun, 13 Jun 2021 16:00:50 +0200 Subject: [PATCH 01/11] Move AccountStore, NIORoom and NIORoomSummary onto the main actor --- Nio.xcodeproj/project.pbxproj | 44 ++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- Nio/Authentication/LoadingView.swift | 6 +- Nio/Authentication/LoginView.swift | 4 +- Nio/NewConversation/NewConversationView.swift | 17 ++- Nio/Settings/SettingsView.swift | 14 +- NioKit/Extensions/MXAutoDiscovery+Async.swift | 17 +++ NioKit/Extensions/MXRestClient+Async.swift | 32 +++++ NioKit/Extensions/MXRoom+Async.swift | 26 ++++ NioKit/Extensions/MXSession+Async.swift | 60 ++++++++ NioKit/Models/NIORoom.swift | 34 ++++- NioKit/Models/NIORoomSummary.swift | 1 + NioKit/Session/AccountStore.swift | 132 ++++++++---------- 13 files changed, 294 insertions(+), 97 deletions(-) create mode 100644 NioKit/Extensions/MXAutoDiscovery+Async.swift create mode 100644 NioKit/Extensions/MXRestClient+Async.swift create mode 100644 NioKit/Extensions/MXRoom+Async.swift create mode 100644 NioKit/Extensions/MXSession+Async.swift diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index e5ed49eb..6514f157 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -62,6 +62,14 @@ 4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BFEFD8C246F458000CCF4A0 /* GetURL.js in Resources */ = {isa = PBXBuildFile; fileRef = 4BFEFD8B246F458000CCF4A0 /* GetURL.js */; }; 4BFEFD92246F686000CCF4A0 /* ShareContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */; }; + 9525D2CF26763DCB004055B6 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CD26763D74004055B6 /* MXSession+Async.swift */; }; + 9525D2D026763DCB004055B6 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CD26763D74004055B6 /* MXSession+Async.swift */; }; + 9525D2D126763DD0004055B6 /* MXRestClient+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */; }; + 9525D2D226763DD0004055B6 /* MXRestClient+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */; }; + 9525D2D326763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C726763CD4004055B6 /* MXAutoDiscovery+Async.swift */; }; + 9525D2D426763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C726763CD4004055B6 /* MXAutoDiscovery+Async.swift */; }; + 9525D2D526763DD0004055B6 /* MXRoom+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */; }; + 9525D2D626763DD0004055B6 /* MXRoom+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */; }; A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */; }; A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */; }; A58352A625A667AB00533363 /* MatrixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A58352A525A667AB00533363 /* MatrixSDK */; }; @@ -360,6 +368,10 @@ 4BFEFD8F246F5EE000CCF4A0 /* NioShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioShareExtension.entitlements; sourceTree = ""; }; 4BFEFD90246F5EF500CCF4A0 /* Nio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nio.entitlements; sourceTree = ""; }; 4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContentView.swift; sourceTree = ""; }; + 9525D2C726763CD4004055B6 /* MXAutoDiscovery+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXAutoDiscovery+Async.swift"; sourceTree = ""; }; + 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRoom+Async.swift"; sourceTree = ""; }; + 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRestClient+Async.swift"; sourceTree = ""; }; + 9525D2CD26763D74004055B6 /* MXSession+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXSession+Async.swift"; sourceTree = ""; }; A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NioApp.swift; sourceTree = ""; }; A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTextViewWrapper.swift; sourceTree = ""; }; CAAF5BF72478696F006FDC33 /* UITextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewWrapper.swift; sourceTree = ""; }; @@ -803,6 +815,10 @@ CAD6816E246B1BB0001878EB /* Extensions */ = { isa = PBXGroup; children = ( + 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */, + 9525D2C726763CD4004055B6 /* MXAutoDiscovery+Async.swift */, + 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */, + 9525D2CD26763D74004055B6 /* MXSession+Async.swift */, 39BA0722240B3C9A00FD28C6 /* MXCredentials+Keychain.swift */, 392389882386FD3900B2E1DF /* MXClient+Publisher.swift */, 3923898C238859D100B2E1DF /* MX+Identifiable.swift */, @@ -1354,10 +1370,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9525D2D226763DD0004055B6 /* MXRestClient+Async.swift in Sources */, E8B4725A25F26D5A00ACEFCB /* MX+Identifiable.swift in Sources */, E8B4725B25F26D5A00ACEFCB /* Reaction.swift in Sources */, + 9525D2CF26763DCB004055B6 /* MXSession+Async.swift in Sources */, E8B4725525F26D5A00ACEFCB /* MXClient+Publisher.swift in Sources */, E897AA3425F2716F00D11427 /* UXKit.swift in Sources */, + 9525D2D626763DD0004055B6 /* MXRoom+Async.swift in Sources */, E8B4725625F26D5A00ACEFCB /* ReactionEvent.swift in Sources */, E8B4725925F26D5A00ACEFCB /* NIORoomSummary.swift in Sources */, E8B4725725F26D5A00ACEFCB /* EditEvent.swift in Sources */, @@ -1365,6 +1384,7 @@ E8B4725825F26D5A00ACEFCB /* CustomEvent.swift in Sources */, E8B4725F25F26D5A00ACEFCB /* Configuration.swift in Sources */, E8B4725D25F26D5A00ACEFCB /* EventCollection.swift in Sources */, + 9525D2D426763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */, E8B4725E25F26D5A00ACEFCB /* MXEvent+Extensions.swift in Sources */, E8B4725425F26D5A00ACEFCB /* MXCredentials+Keychain.swift in Sources */, E8B4726025F26D5A00ACEFCB /* UserDefaults.swift in Sources */, @@ -1437,10 +1457,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9525D2D126763DD0004055B6 /* MXRestClient+Async.swift in Sources */, E897AA2025F2707C00D11427 /* NIORoomSummary.swift in Sources */, E897AA2525F2707C00D11427 /* ReactionEvent.swift in Sources */, + 9525D2D026763DCB004055B6 /* MXSession+Async.swift in Sources */, E897AA2225F2707C00D11427 /* AccountStore.swift in Sources */, E897AA3525F2716F00D11427 /* UXKit.swift in Sources */, + 9525D2D526763DD0004055B6 /* MXRoom+Async.swift in Sources */, E897AA1E25F2707C00D11427 /* MXClient+Publisher.swift in Sources */, E897AA1B25F2707C00D11427 /* Configuration.swift in Sources */, E897AA1C25F2707C00D11427 /* CustomEvent.swift in Sources */, @@ -1448,6 +1471,7 @@ E897AA1925F2707C00D11427 /* EditEvent.swift in Sources */, E897AA1F25F2707C00D11427 /* MX+Identifiable.swift in Sources */, E897AA1D25F2707C00D11427 /* MXCredentials+Keychain.swift in Sources */, + 9525D2D326763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */, E897AA2425F2707C00D11427 /* MXEvent+Extensions.swift in Sources */, E897AA2125F2707C00D11427 /* EventCollection.swift in Sources */, E897AA2625F2707C00D11427 /* UserDefaults.swift in Sources */, @@ -1712,6 +1736,7 @@ DEVELOPMENT_ASSET_PATHS = "\"Nio/Preview Content\""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Nio/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1736,6 +1761,7 @@ DEVELOPMENT_TEAM = HU85FER47E; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Nio/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1756,7 +1782,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 33; INFOPLIST_FILE = NioShareExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1778,7 +1804,7 @@ CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = HU85FER47E; INFOPLIST_FILE = NioShareExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1843,7 +1869,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4GXF3JAMM4; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_MODULE_NAME = NioKit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1858,7 +1884,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4GXF3JAMM4; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_MODULE_NAME = NioKit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1884,7 +1910,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_BUNDLE_IDENTIFIER = "$(MAC_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1908,7 +1934,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_BUNDLE_IDENTIFIER = chat.nio.mio; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1922,7 +1948,8 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4GXF3JAMM4; EXECUTABLE_PREFIX = lib; - MACOSX_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_MODULE_NAME = NioKit; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -1936,7 +1963,8 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 4GXF3JAMM4; EXECUTABLE_PREFIX = lib; - MACOSX_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_MODULE_NAME = NioKit; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/Nio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 99d97960..13769d8c 100644 --- a/Nio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -65,7 +65,7 @@ } }, { - "package": "cmark", + "package": "swift-cmark", "repositoryURL": "https://github.com/SwiftDocOrg/swift-cmark.git", "state": { "branch": null, @@ -74,7 +74,7 @@ } }, { - "package": "Introspect", + "package": "SwiftUI-Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", "state": { "branch": null, diff --git a/Nio/Authentication/LoadingView.swift b/Nio/Authentication/LoadingView.swift index 49769fe8..ca821ae4 100644 --- a/Nio/Authentication/LoadingView.swift +++ b/Nio/Authentication/LoadingView.swift @@ -35,7 +35,11 @@ struct LoadingView: View { Spacer() - Button(action: self.store.logout) { + Button(action: { + asyncDetached { + await self.store.logout() + } + }) { Text(verbatim: L10n.Loading.cancel).font(.callout) } .padding() diff --git a/Nio/Authentication/LoginView.swift b/Nio/Authentication/LoginView.swift index d61ad82b..dbb7acaf 100644 --- a/Nio/Authentication/LoginView.swift +++ b/Nio/Authentication/LoginView.swift @@ -37,7 +37,9 @@ struct LoginContainerView: View { return } - store.login(username: username, password: password, homeserver: homeserverURL) + async { + await store.login(username: username, password: password, homeserver: homeserverURL) + } } private func guessHomeserverURL() { diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 9e4cc291..780117db 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -28,10 +28,12 @@ private struct NewConversationView: View { @Binding var createdRoomId: ObjectIdentifier? @State private var errorMessage: String? + @MainActor private var usersFooter: some View { Text("\(L10n.NewConversation.forExample) \(store?.session?.myUserId ?? "@username:server.org")") } + @MainActor private var form: some View { Form { Section(footer: usersFooter) { @@ -65,7 +67,12 @@ private struct NewConversationView: View { Section { HStack { #if !os(macOS) - Button(action: createRoom) { + // Seems to be a bug in Xcode, currently needs to be asnyc, to be able to await actor + Button(action: { + async { + createRoom + } + }) { Text(verbatim: L10n.NewConversation.createRoom) } .disabled(users.contains("") || (roomName.isEmpty && users.count > 1)) @@ -94,7 +101,11 @@ private struct NewConversationView: View { } } ToolbarItem(placement: .confirmationAction) { - Button(action: createRoom) { + Button(action: { + async { + await createRoom + } + }) { Text(verbatim: L10n.NewConversation.createRoom) } .disabled(users.contains("") || (roomName.isEmpty && users.count > 1)) @@ -140,7 +151,7 @@ private struct NewConversationView: View { } } - private func createRoom() { + @MainActor private func createRoom() { isWaiting = true let parameters = MXRoomCreationParameters() diff --git a/Nio/Settings/SettingsView.swift b/Nio/Settings/SettingsView.swift index 636c51d1..19b20ac6 100644 --- a/Nio/Settings/SettingsView.swift +++ b/Nio/Settings/SettingsView.swift @@ -5,10 +5,18 @@ struct SettingsContainerView: View { @EnvironmentObject var store: AccountStore var body: some View { - #if os(macOS) - MacSettingsView(logoutAction: self.store.logout) + #if os(macOS) + MacSettingsView(logoutAction: { + async { + await self.store.logout() + } + }) #else - SettingsView(logoutAction: self.store.logout) + SettingsView(logoutAction: { + async { + await self.store.logout() + } + }) #endif } } diff --git a/NioKit/Extensions/MXAutoDiscovery+Async.swift b/NioKit/Extensions/MXAutoDiscovery+Async.swift new file mode 100644 index 00000000..9129eca5 --- /dev/null +++ b/NioKit/Extensions/MXAutoDiscovery+Async.swift @@ -0,0 +1,17 @@ +// +// MXAutoDiscovery+Async.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +import MatrixSDK + +extension MXAutoDiscovery { + public func findClientConfig() async throws -> MXDiscoveredClientConfig { + return try await withCheckedThrowingContinuation {continuation in + self.findClientConfig({ continuation.resume(returning: $0)}, failure: {continuation.resume(throwing: $0)}) + } + } +} diff --git a/NioKit/Extensions/MXRestClient+Async.swift b/NioKit/Extensions/MXRestClient+Async.swift new file mode 100644 index 00000000..c14d8679 --- /dev/null +++ b/NioKit/Extensions/MXRestClient+Async.swift @@ -0,0 +1,32 @@ +// +// MXRestClient+async.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +import MatrixSDK + +extension MXRestClient { + func login(type loginType: MatrixSDK.MXLoginFlowType = .password, username: String, password: String) async throws -> MXCredentials { + return try await withCheckedThrowingContinuation {continuation in + self.login(type: loginType, username: username, password: password, completion: {resp in + switch resp { + case .success(let v): + continuation.resume(returning: v) + case .failure(let e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } + + func wellKnown() async throws -> MXWellKnown { + return try await withCheckedThrowingContinuation {continuation in + self.wellKnow({continuation.resume(returning: $0!)}, failure: {continuation.resume(throwing: $0!)}) + } + } +} diff --git a/NioKit/Extensions/MXRoom+Async.swift b/NioKit/Extensions/MXRoom+Async.swift new file mode 100644 index 00000000..89bff57b --- /dev/null +++ b/NioKit/Extensions/MXRoom+Async.swift @@ -0,0 +1,26 @@ +// +// MXRoom+Asnyc.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +import MatrixSDK + +extension MXRoom { + func members() async throws -> MXRoomMembers? { + return try await withCheckedThrowingContinuation {continuation in + self.members(completion: {resp in + switch resp { + case .success(let v): + continuation.resume(returning: v) + case .failure(let e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } +} diff --git a/NioKit/Extensions/MXSession+Async.swift b/NioKit/Extensions/MXSession+Async.swift new file mode 100644 index 00000000..dc5e51f6 --- /dev/null +++ b/NioKit/Extensions/MXSession+Async.swift @@ -0,0 +1,60 @@ +// +// MXSession+async.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +import MatrixSDK + +extension MXSession { + public func logout() async throws { + return try await withCheckedThrowingContinuation {continuation in + self.logout(completion: {resp in + switch resp { + case .success(_): + continuation.resume() + case .failure(let e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } + + public func setStore(_ store: MXStore) async throws { + return try await withCheckedThrowingContinuation {continuation in + self.setStore(store, completion: {resp in + switch resp { + case .success(_): + continuation.resume() + case .failure(let e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } + + public func start(withSyncFilterId filterId: String? = nil) async throws { + return try await withCheckedThrowingContinuation {continuation in + self.start(withSyncFilterId: filterId) {resp in + switch resp { + case .success(_): + continuation.resume() + case .failure(let e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + } + } + } +} + +struct NioUnknownContinuationSwitchError: Error { + let value: Any +} diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index 1c8f588c..3c299481 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -20,8 +20,9 @@ public struct RoomItem: Codable, Hashable { } } +@MainActor public class NIORoom: ObservableObject { - public var room: MXRoom + public let room: MXRoom @Published public var summary: NIORoomSummary @@ -29,10 +30,33 @@ public class NIORoom: ObservableObject { @Published internal var eventCache: [MXEvent] = [] + // MARK: - computed vars public var isDirect: Bool { room.isDirect } + public var isEncrypted: Bool { + room.summary.isEncrypted + } + + public var displayName: String { + room.summary.displayname + } + + public var avatarUrl: URL? { + get { + guard let avatar = (self.room.summary.avatar ?? nil) else { + return nil + } + + if avatar.starts(with: "http") { + return URL(string: avatar) + } + + return URL(string: self.room.mxSession.mediaManager.url(ofContent: avatar)) + } + } + public var lastMessage: String { if summary.membership == .invite { let inviteEvent = eventCache.last { @@ -53,6 +77,7 @@ public class NIORoom: ObservableObject { } } + // MARK: - init public init(_ room: MXRoom) { self.room = room self.summary = NIORoomSummary(room.summary) @@ -88,6 +113,7 @@ public class NIORoom: ObservableObject { objectWillChange.send() // room.outgoingMessages() will change var localEcho: MXEvent? = nil + // TODO: async room.sendTextMessage(text, localEcho: &localEcho) { _ in self.objectWillChange.send() // localEcho.sentState has(!) changed } @@ -99,6 +125,7 @@ public class NIORoom: ObservableObject { objectWillChange.send() // room.outgoingMessages() will change var localEcho: MXEvent? = nil + // TODO: async room.sendEvent(.reaction, content: content, localEcho: &localEcho) { _ in self.objectWillChange.send() // localEcho.sentState has(!) changed } @@ -111,10 +138,12 @@ public class NIORoom: ObservableObject { // swiftlint:disable:next force_try let content = try! EditEvent(eventId: eventId, text: text).encodeContent() // TODO: Use localEcho to show sent message until it actually comes back + // TODO: async room.sendMessage(withContent: content, localEcho: &localEcho) { _ in } } public func redact(eventId: String, reason: String?) { + // TODO: async room.redactEvent(eventId, reason: reason) { _ in } } @@ -123,6 +152,7 @@ public class NIORoom: ObservableObject { var localEcho: MXEvent? = nil objectWillChange.send() // room.outgoingMessages() will change + // TODO: async room.sendImage( data: imageData, size: image.size, @@ -145,7 +175,7 @@ public class NIORoom: ObservableObject { } extension NIORoom: Identifiable { - public var id: ObjectIdentifier { + public nonisolated var id: ObjectIdentifier { room.id } } diff --git a/NioKit/Models/NIORoomSummary.swift b/NioKit/Models/NIORoomSummary.swift index 2397c854..22fe2143 100644 --- a/NioKit/Models/NIORoomSummary.swift +++ b/NioKit/Models/NIORoomSummary.swift @@ -2,6 +2,7 @@ import Foundation import MatrixSDK @dynamicMemberLookup +@MainActor public class NIORoomSummary: ObservableObject { internal var summary: MXRoomSummary diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 2facdeed..781fd4e9 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -10,6 +10,7 @@ public enum LoginState { case loggedIn(userId: String) } +@MainActor public class AccountStore: ObservableObject { public var client: MXRestClient? public var session: MXSession? @@ -30,19 +31,18 @@ public class AccountStore: ObservableObject { } Configuration.setupMatrixSDKSettings() - - if let credentials = MXCredentials.from(keychain) { - self.loginState = .authenticating - self.credentials = credentials - self.sync { result in - switch result { - case .failure(let error): - print("Error on starting session with saved credentials: \(error)") - self.loginState = .failure(error) - case .success(let state): - self.loginState = state - self.session?.crypto.warnOnUnknowDevices = false - } + guard let credentials = MXCredentials.from(keychain) else { + return + } + self.credentials = credentials + self.loginState = .authenticating + async { + do { + self.loginState = try await self.sync() + self.session?.crypto.warnOnUnknowDevices = false + } catch { + print("Error on starting session with saved credentials: \(error)") + self.loginState = .failure(error) } } } @@ -55,91 +55,64 @@ public class AccountStore: ObservableObject { @Published public var loginState: LoginState = .loggedOut - public func login(username: String, password: String, homeserver: URL) { + public func login(username: String, password: String, homeserver: URL) async { self.loginState = .authenticating self.client = MXRestClient(homeServer: homeserver, unrecognizedCertificateHandler: nil) - self.client?.login(username: username, password: password) { response in - switch response { - case .failure(let error): - print("Error on starting session with new credentials: \(error)") - self.loginState = .failure(error) - case .success(let credentials): - self.credentials = credentials - credentials.save(to: self.keychain) - print("Error on starting session with new credentials:") - - self.sync { result in - switch result { - case .failure(let error): - // Does this make sense? The login itself didn't fail, but syncing did. - self.loginState = .failure(error) - case .success(let state): - self.loginState = state - self.session?.crypto.warnOnUnknowDevices = false - } - } - @unknown default: - fatalError("Unexpected Matrix response: \(response)") + self.client?.acceptableContentTypes = ["text/plain", "text/html", "application/json", "application/octet-stream", "any"] + + do { + let credentials = try await self.client?.login(username: username, password: password) + guard let credentials = credentials else { + self.loginState = .failure(AccountStoreError.noCredentials) + return } + self.credentials = credentials + credentials.save(to: self.keychain) + self.loginState = try await self.sync() + self.session?.crypto.warnOnUnknowDevices = false + } catch { + self.loginState = .failure(error) } } - public func logout(completion: @escaping (Result) -> Void) { + public func logout() async { self.credentials?.clear(from: keychain) - self.session?.logout { response in - switch response { - case .failure(let error): - completion(.failure(error)) - case .success: - self.fileStore?.deleteAllData() - completion(.success(.loggedOut)) - @unknown default: - fatalError("Unexpected Matrix response: \(response)") - } + do { + try await self.session?.logout() + self.loginState = .loggedOut + } catch { + // Close the session even if the logout request failed + self.loginState = .loggedOut } } - public func logout() { - self.logout { result in - switch result { - case .failure: - // Close the session even if the logout request failed - self.loginState = .loggedOut - case .success(let state): - self.loginState = state + @available(*, deprecated, message: "Prefer async alternative instead") + private func sync(completion: @escaping (Result) -> Void) { + async { + do { + let result = try await sync() + completion(.success(result)) + } catch { + completion(.failure(error)) } } } - private func sync(completion: @escaping (Result) -> Void) { - guard let credentials = self.credentials else { return } + + private func sync() async throws -> LoginState { + guard let credentials = self.credentials else { + throw AccountStoreError.noCredentials + } self.client = MXRestClient(credentials: credentials, unrecognizedCertificateHandler: nil) self.session = MXSession(matrixRestClient: self.client!) self.fileStore = MXFileStore() - self.session!.setStore(fileStore!) { response in - switch response { - case .failure(let error): - completion(.failure(error)) - case .success: - self.session?.start { response in - switch response { - case .failure(let error): - completion(.failure(error)) - case .success: - let userId = credentials.userId! - completion(.success(.loggedIn(userId: userId))) - @unknown default: - fatalError("Unexpected Matrix response: \(response)") - } - } - @unknown default: - fatalError("Unexpected Matrix response: \(response)") - } - } + try await self.session!.setStore(fileStore!) + try await self.session?.start() + return .loggedIn(userId: credentials.userId!) } // MARK: - Rooms @@ -201,3 +174,8 @@ public class AccountStore: ObservableObject { } } } + +enum AccountStoreError: Error { + case noCredentials + case invalidUrl +} From 7178dfef76a441e2b306ea9a461a923fa29cd2c3 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Sun, 13 Jun 2021 17:34:25 +0200 Subject: [PATCH 02/11] move macOS code into Mio folder --- Mio/Conversations/RecentRoomsView.swift | 69 +++++++ Mio/Conversations/RoomContainerView.swift | 84 +++++++++ Mio/Settings/SettingsContainerView.swift | 61 ++++++ Mio/Shared Views/ImagePicker.swift | 22 +++ Mio/Shared Views/MessageTextViewWrapper.swift | 50 +++++ Nio.xcodeproj/project.pbxproj | 90 ++++++--- .../RecentRoomsContainerView.swift | 139 ++++++++++++++ Nio/Conversations/RecentRoomsView.swift | 173 +----------------- Nio/Conversations/RoomContainerView.swift | 92 ++++++++++ Nio/Conversations/RoomView.swift | 130 +------------ ...View.swift => SettingsContainerView.swift} | 56 ++---- Nio/Shared Views/ImagePicker.swift | 11 -- Nio/Shared Views/MessageTextViewWrapper.swift | 41 ----- 13 files changed, 612 insertions(+), 406 deletions(-) create mode 100644 Mio/Conversations/RecentRoomsView.swift create mode 100644 Mio/Conversations/RoomContainerView.swift create mode 100644 Mio/Settings/SettingsContainerView.swift create mode 100644 Mio/Shared Views/ImagePicker.swift create mode 100644 Mio/Shared Views/MessageTextViewWrapper.swift create mode 100644 Nio/Conversations/RecentRoomsContainerView.swift create mode 100644 Nio/Conversations/RoomContainerView.swift rename Nio/Settings/{SettingsView.swift => SettingsContainerView.swift} (60%) diff --git a/Mio/Conversations/RecentRoomsView.swift b/Mio/Conversations/RecentRoomsView.swift new file mode 100644 index 00000000..c89c7325 --- /dev/null +++ b/Mio/Conversations/RecentRoomsView.swift @@ -0,0 +1,69 @@ +// +// RecentRoomsView.swift +// Mio +// +// Created by Finn Behrens on 13.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import SwiftUI +import MatrixSDK + +import NioKit + +struct RecentRoomsView: View { + @EnvironmentObject var store: AccountStore + + @Binding var selectedNavigationItem: SelectedNavigationItem? + @Binding var selectedRoomId: ObjectIdentifier? + + let rooms: [NIORoom] + + private var joinedRooms: [NIORoom] { + rooms.filter {$0.room.summary.membership == .join} + } + + private var invitedRooms: [NIORoom] { + rooms.filter {$0.room.summary.membership == .invite} + } + + var body: some View { + NavigationView { + List { + if !invitedRooms.isEmpty { + RoomsListSection( + sectionHeader: L10n.RecentRooms.PendingInvitations.header, + rooms: invitedRooms, + onLeaveAlertTitle: L10n.RecentRooms.PendingInvitations.Leave.alertTitle, + selectedRoomId: $selectedRoomId + ) + } + + RoomsListSection( + sectionHeader: invitedRooms.isEmpty ? nil : L10n.RecentRooms.Rooms.header , + rooms: joinedRooms, + onLeaveAlertTitle: L10n.RecentRooms.Leave.alertTitle, + selectedRoomId: $selectedRoomId + ) + + } + .listStyle(SidebarListStyle()) + .navigationTitle("Mio") + .frame(minWidth: Style.minSidebarWidth) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(action: { self.selectedNavigationItem = .newConversation }) { + Label(L10n.RecentRooms.AccessibilityLabel.newConversation, + systemImage: SFSymbol.newConversation.rawValue) + } + } + } + } + } +} + +struct RecentRoomsView_Previews: PreviewProvider { + static var previews: some View { + RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), rooms: []) + } +} diff --git a/Mio/Conversations/RoomContainerView.swift b/Mio/Conversations/RoomContainerView.swift new file mode 100644 index 00000000..9ee31695 --- /dev/null +++ b/Mio/Conversations/RoomContainerView.swift @@ -0,0 +1,84 @@ +import SwiftUI +import Combine +import MatrixSDK + +import NioKit + +struct RoomContainerView: View { + @ObservedObject var room: NIORoom + + @State private var showAttachmentPicker = false + @State private var showImagePicker = false + @State private var eventToReactTo: String? + @State private var showJoinAlert = false + + private var roomView: RoomView { + RoomView( + events: room.events(), + isDirect: room.isDirect, + showAttachmentPicker: $showAttachmentPicker, + onCommit: { message in + self.room.send(text: message) + }, + onReact: { eventId in + self.eventToReactTo = eventId + }, + onRedact: { eventId, reason in + self.room.redact(eventId: eventId, reason: reason) + }, + onEdit: { message, eventId in + self.room.edit(text: message, eventId: eventId) + } + ) + } + + var body: some View { + VStack(spacing: 0) { + Divider() // TBD: This might be better done w/ toolbar styling + roomView + } + .navigationTitle(Text(room.summary.displayname ?? "")) + // TODO: action sheet + .sheet(item: $eventToReactTo) { eventId in + ReactionPicker { reaction in + self.room.react(toEventId: eventId, emoji: reaction) + self.eventToReactTo = nil + } + } + // TODO: join alert + .onAppear { + switch self.room.summary.membership { + case .invite: + self.showJoinAlert = true + case .join: + self.room.markAllAsRead() + default: + break + } + } + .environmentObject(room) + // TODO: background sheet thing + .background(Color(.textBackgroundColor)) + .frame(minWidth: Style.minTimelineWidth) + } + + // TODO: port me to macOS + /* + private var attachmentPickerSheet: ActionSheet { + ActionSheet( + title: Text(verbatim: L10n.Room.Attachment.selectType), buttons: [ + .default(Text(verbatim: L10n.Room.Attachment.typePhoto), action: { + self.showImagePicker = true + }), + .cancel() + ] + ) + }*/ +} + + +/*struct RecentRoomsContainerView_Previews: PreviewProvider { + static var previews: some View { + RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), rooms: []) + } +}*/ diff --git a/Mio/Settings/SettingsContainerView.swift b/Mio/Settings/SettingsContainerView.swift new file mode 100644 index 00000000..900fd6db --- /dev/null +++ b/Mio/Settings/SettingsContainerView.swift @@ -0,0 +1,61 @@ +// +// SettingsContainerView.swift +// Mio +// +// Created by Finn Behrens on 13.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import SwiftUI + +import NioKit + +struct SettingsContainerView: View { + @EnvironmentObject var store: AccountStore + + var body: some View { + MacSettingsView(logoutAction: { + async { + await self.store.logout() + } + }) + } +} + +private struct MacSettingsView: View { + @AppStorage("accentColor") private var accentColor: Color = .purple + let logoutAction: () -> Void + + var body: some View { + Form { + Section { + Picker(selection: $accentColor, label: Text(verbatim: L10n.Settings.accentColor)) { + ForEach(Color.allAccentOptions, id: \.self) { color in + HStack { + Circle() + .frame(width: 20) + .foregroundColor(color) + Text(color.description.capitalized) + } + .tag(color) + } + } + // No icon picker on macOS + } + + Section { + Button(action: self.logoutAction) { + Text(verbatim: L10n.Settings.logOut) + } + } + } + .padding() + .frame(maxWidth: 320) + } +} + +struct SettingsContainerView_Previews: PreviewProvider { + static var previews: some View { + SettingsContainerView() + } +} diff --git a/Mio/Shared Views/ImagePicker.swift b/Mio/Shared Views/ImagePicker.swift new file mode 100644 index 00000000..be78a756 --- /dev/null +++ b/Mio/Shared Views/ImagePicker.swift @@ -0,0 +1,22 @@ +// +// ImagePicker.swift +// Mio +// +// Created by Finn Behrens on 13.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import SwiftUI + +struct ImagePicker: View { + + var body: some View { + Text("Sorry, no image picker on macOS yet :-/") + } +} + +struct ImagePicker_Previews: PreviewProvider { + static var previews: some View { + ImagePicker() + } +} diff --git a/Mio/Shared Views/MessageTextViewWrapper.swift b/Mio/Shared Views/MessageTextViewWrapper.swift new file mode 100644 index 00000000..70a1e0f1 --- /dev/null +++ b/Mio/Shared Views/MessageTextViewWrapper.swift @@ -0,0 +1,50 @@ +// +// MessageTextViewWrapper.swift +// Mio +// +// Created by Finn Behrens on 13.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import SwiftUI +import NioKit + +class MessageTextView: NSTextView { + convenience init(attributedString: NSAttributedString, linkColor: UXColor, + maxSize: CGSize) + { + self.init() + backgroundColor = .clear + textContainerInset = .zero + isEditable = false + linkTextAttributes = [ + .foregroundColor: linkColor, + .underlineStyle: NSUnderlineStyle.single.rawValue, + ] + + self.insertText(attributedString, + replacementRange: NSRange(location: 0, length: 0)) + self.maxSize = maxSize + + // don't resist text wrapping across multiple lines + setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } +} + +struct MessageTextViewWrapper: NSViewRepresentable { + let attributedString: NSAttributedString + let linkColor: NSColor + let maxSize: CGSize + + func makeNSView(context: Context) -> MessageTextView { + MessageTextView(attributedString: attributedString, linkColor: linkColor, maxSize: maxSize) + } + + func updateNSView(_ uiView: MessageTextView, context: Context) { + // nothing to update + } + + func makeCoordinator() { + // nothing to coordinate + } +} diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 6514f157..e7fe73f3 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 3902B89C2393FE6100698B87 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B89B2393FE6100698B87 /* MessageView.swift */; }; 3902B89E2393FE8200698B87 /* GenericEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B89D2393FE8200698B87 /* GenericEventView.swift */; }; 3902B8A0239410EE00698B87 /* ContentSizeCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B89F239410EE00698B87 /* ContentSizeCategory.swift */; }; - 3902B8A32395935600698B87 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B8A22395935600698B87 /* SettingsView.swift */; }; 3902B8A52395A77800698B87 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B8A42395A77800698B87 /* LoadingView.swift */; }; 390D63BF246F4BEE00B8F640 /* Sketch@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 390D63BD246F4BEE00B8F640 /* Sketch@3x.png */; }; 390D63C0246F4BEE00B8F640 /* Sketch@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 390D63BE246F4BEE00B8F640 /* Sketch@2x.png */; }; @@ -44,8 +43,8 @@ 39C931E92384328B004449E1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 39C931E72384328B004449E1 /* LaunchScreen.storyboard */; }; 39C931F523846966004449E1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C931F423846966004449E1 /* LoginView.swift */; }; 39C931F723846B2D004449E1 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 39C931F623846B2D004449E1 /* .swiftlint.yml */; }; - 39C932072384BB13004449E1 /* RecentRoomsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsView.swift */; }; - 39C93209238553E4004449E1 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C93208238553E4004449E1 /* RoomView.swift */; }; + 39C932072384BB13004449E1 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; + 39C93209238553E4004449E1 /* RoomContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C93208238553E4004449E1 /* RoomContainerView.swift */; }; 39C9320B23856033004449E1 /* MessageComposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C9320A23856033004449E1 /* MessageComposerView.swift */; }; 39D166C62385C804006DD257 /* String+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D166C52385C804006DD257 /* String+Emoji.swift */; }; 39D166C82385C832006DD257 /* EventContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D166C72385C832006DD257 /* EventContainerView.swift */; }; @@ -70,6 +69,16 @@ 9525D2D426763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C726763CD4004055B6 /* MXAutoDiscovery+Async.swift */; }; 9525D2D526763DD0004055B6 /* MXRoom+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */; }; 9525D2D626763DD0004055B6 /* MXRoom+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */; }; + 9525D2D82676548B004055B6 /* RecentRoomsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2D72676548B004055B6 /* RecentRoomsView.swift */; }; + 9525D2DB267654E9004055B6 /* RecentRoomsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2DA267654E9004055B6 /* RecentRoomsView.swift */; }; + 9525D2DE26765700004055B6 /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2DD26765700004055B6 /* MessageTextViewWrapper.swift */; }; + 9525D2E026765754004055B6 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2DF26765754004055B6 /* ImagePicker.swift */; }; + 9525D2E226765808004055B6 /* RoomContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E126765808004055B6 /* RoomContainerView.swift */; }; + 9525D2E426765850004055B6 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E326765850004055B6 /* RoomView.swift */; }; + 9525D2E526765850004055B6 /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E326765850004055B6 /* RoomView.swift */; }; + 9525D2EC267659A2004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */; }; + 9525D2ED267659BD004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E62676594F004055B6 /* SettingsContainerView.swift */; }; + 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */; }; A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */; }; A58352A625A667AB00533363 /* MatrixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A58352A525A667AB00533363 /* MatrixSDK */; }; @@ -111,9 +120,7 @@ E843F2FC25F2780C00B0F33B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B8A42395A77800698B87 /* LoadingView.swift */; }; E843F2FD25F2781100B0F33B /* IndividuallyRoundedRectangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC46D5F23A276B30079C24F /* IndividuallyRoundedRectangle.swift */; }; E843F2FE25F2781400B0F33B /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF2AE882458EEBB00D84133 /* AttributedText.swift */; }; - E843F2FF25F2781400B0F33B /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */; }; E843F30025F2781400B0F33B /* MultilineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0A2E46245E2EF800A79443 /* MultilineTextField.swift */; }; - E843F30225F2781400B0F33B /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29F5B42466EC240084043B /* ImagePicker.swift */; }; E843F30325F2781400B0F33B /* MarkdownText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF2AE8A2458EF0900D84133 /* MarkdownText.swift */; }; E843F30425F2781400B0F33B /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392389CB238EBB1400B2E1DF /* ReverseList.swift */; }; E843F30525F2781400B0F33B /* SFSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 395E06FC25DDC1790059F6AD /* SFSymbol.swift */; }; @@ -122,8 +129,6 @@ E843F30825F2781800B0F33B /* PeekableIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC46D6A23A55E930079C24F /* PeekableIterator.swift */; }; E843F30925F2781800B0F33B /* MXURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984654423B7ECBA006C173B /* MXURL.swift */; }; E843F30A25F2781800B0F33B /* Formatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392389932388899200B2E1DF /* Formatter.swift */; }; - E843F30B25F2781B00B0F33B /* RoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C93208238553E4004449E1 /* RoomView.swift */; }; - E843F30C25F2781B00B0F33B /* RecentRoomsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsView.swift */; }; E843F30D25F2781B00B0F33B /* RoomListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3923898E2388707E00B2E1DF /* RoomListItemView.swift */; }; E843F30E25F2781F00B0F33B /* EventContextMenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392221AF243A6F8E004D8794 /* EventContextMenuModel.swift */; }; E843F30F25F2782000B0F33B /* ReactionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3922219F2437285B004D8794 /* ReactionPicker.swift */; }; @@ -147,7 +152,6 @@ E843F32125F2782E00B0F33B /* RoomTopicEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392221B1243D0CCC004D8794 /* RoomTopicEventView.swift */; }; E843F32225F2782E00B0F33B /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF2AE91245AEEBA00D84133 /* BadgeView.swift */; }; E843F32325F2783200B0F33B /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DD77AE247006E300A29DEE /* AppIcon.swift */; }; - E843F32425F2783200B0F33B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3902B8A22395935600698B87 /* SettingsView.swift */; }; E843F32525F2783500B0F33B /* NewConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD867262460ADEF0014E3D6 /* NewConversationView.swift */; }; E843F32625F2783900B0F33B /* String+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D166C52385C804006DD257 /* String+Emoji.swift */; }; E843F32725F2783900B0F33B /* Color+allAccent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BA0726240B534600FD28C6 /* Color+allAccent.swift */; }; @@ -280,7 +284,6 @@ 3902B89B2393FE6100698B87 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 3902B89D2393FE8200698B87 /* GenericEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericEventView.swift; sourceTree = ""; }; 3902B89F239410EE00698B87 /* ContentSizeCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizeCategory.swift; sourceTree = ""; }; - 3902B8A22395935600698B87 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 3902B8A42395A77800698B87 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 3902D4E5248277310009355A /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 3902F94D25ACE72B009F5991 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -342,8 +345,8 @@ 39C931EA2384328B004449E1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39C931F423846966004449E1 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; usesTabs = 0; }; 39C931F623846B2D004449E1 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; }; - 39C932062384BB13004449E1 /* RecentRoomsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRoomsView.swift; sourceTree = ""; }; - 39C93208238553E4004449E1 /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; + 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRoomsContainerView.swift; sourceTree = ""; }; + 39C93208238553E4004449E1 /* RoomContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContainerView.swift; sourceTree = ""; }; 39C9320A23856033004449E1 /* MessageComposerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerView.swift; sourceTree = ""; }; 39D166C52385C804006DD257 /* String+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Emoji.swift"; sourceTree = ""; }; 39D166C72385C832006DD257 /* EventContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventContainerView.swift; sourceTree = ""; }; @@ -372,6 +375,14 @@ 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRoom+Async.swift"; sourceTree = ""; }; 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRestClient+Async.swift"; sourceTree = ""; }; 9525D2CD26763D74004055B6 /* MXSession+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXSession+Async.swift"; sourceTree = ""; }; + 9525D2D72676548B004055B6 /* RecentRoomsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRoomsView.swift; sourceTree = ""; }; + 9525D2DA267654E9004055B6 /* RecentRoomsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRoomsView.swift; sourceTree = ""; }; + 9525D2DD26765700004055B6 /* MessageTextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTextViewWrapper.swift; sourceTree = ""; }; + 9525D2DF26765754004055B6 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 9525D2E126765808004055B6 /* RoomContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContainerView.swift; sourceTree = ""; }; + 9525D2E326765850004055B6 /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; + 9525D2E62676594F004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; + 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NioApp.swift; sourceTree = ""; }; A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTextViewWrapper.swift; sourceTree = ""; }; CAAF5BF72478696F006FDC33 /* UITextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewWrapper.swift; sourceTree = ""; }; @@ -483,8 +494,8 @@ 3902B8A12395934900698B87 /* Settings */ = { isa = PBXGroup; children = ( - 3902B8A22395935600698B87 /* SettingsView.swift */, 39DD77AE247006E300A29DEE /* AppIcon.swift */, + 9525D2E62676594F004055B6 /* SettingsContainerView.swift */, ); path = Settings; sourceTree = ""; @@ -703,13 +714,15 @@ 39C932052384BAA0004449E1 /* Conversations */ = { isa = PBXGroup; children = ( - 39C932062384BB13004449E1 /* RecentRoomsView.swift */, + 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */, 3923898E2388707E00B2E1DF /* RoomListItemView.swift */, - 39C93208238553E4004449E1 /* RoomView.swift */, + 39C93208238553E4004449E1 /* RoomContainerView.swift */, 3922219E24372834004D8794 /* ContextMenu */, 39C9320A23856033004449E1 /* MessageComposerView.swift */, 39B834BF243FC42000AE1EA0 /* TypingIndicatorView.swift */, 3907AB482393FE0E00B25DE9 /* Event Views */, + 9525D2D72676548B004055B6 /* RecentRoomsView.swift */, + 9525D2E326765850004055B6 /* RoomView.swift */, ); path = Conversations; sourceTree = ""; @@ -784,6 +797,32 @@ path = NioShareExtension; sourceTree = ""; }; + 9525D2D9267654D3004055B6 /* Conversations */ = { + isa = PBXGroup; + children = ( + 9525D2DA267654E9004055B6 /* RecentRoomsView.swift */, + 9525D2E126765808004055B6 /* RoomContainerView.swift */, + ); + path = Conversations; + sourceTree = ""; + }; + 9525D2DC267656EE004055B6 /* Shared Views */ = { + isa = PBXGroup; + children = ( + 9525D2DD26765700004055B6 /* MessageTextViewWrapper.swift */, + 9525D2DF26765754004055B6 /* ImagePicker.swift */, + ); + path = "Shared Views"; + sourceTree = ""; + }; + 9525D2EA26765994004055B6 /* Settings */ = { + isa = PBXGroup; + children = ( + 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */, + ); + path = Settings; + sourceTree = ""; + }; A58352AA25A667B300533363 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -874,6 +913,9 @@ E843F2DB25F2748200B0F33B /* Mio */ = { isa = PBXGroup; children = ( + 9525D2EA26765994004055B6 /* Settings */, + 9525D2DC267656EE004055B6 /* Shared Views */, + 9525D2D9267654D3004055B6 /* Conversations */, E843F2E025F2748300B0F33B /* Assets.xcassets */, E843F2E525F2748300B0F33B /* Info.plist */, E843F2E625F2748300B0F33B /* Mio.entitlements */, @@ -1305,7 +1347,7 @@ CAFCB323245F6E6700869320 /* NSAttributedString+Extensions.swift in Sources */, CAF2AE892458EEBC00D84133 /* AttributedText.swift in Sources */, CAC46D5B23A2734C0079C24F /* EnvironmentValues.swift in Sources */, - 39C932072384BB13004449E1 /* RecentRoomsView.swift in Sources */, + 39C932072384BB13004449E1 /* RecentRoomsContainerView.swift in Sources */, CAC46D6F23A56BBE0079C24F /* GroupingIterator.swift in Sources */, 392221B2243D0CCC004D8794 /* RoomTopicEventView.swift in Sources */, 3902B8A52395A77800698B87 /* LoadingView.swift in Sources */, @@ -1319,7 +1361,7 @@ 392221A02437285B004D8794 /* ReactionPicker.swift in Sources */, 3902B89C2393FE6100698B87 /* MessageView.swift in Sources */, CAC46D5723A272D10079C24F /* BorderlessMessageView.swift in Sources */, - 39C93209238553E4004449E1 /* RoomView.swift in Sources */, + 39C93209238553E4004449E1 /* RoomContainerView.swift in Sources */, 392221AA2438083C004D8794 /* RedactionEventView.swift in Sources */, 392389CC238EBB1500B2E1DF /* ReverseList.swift in Sources */, 392389942388899200B2E1DF /* Formatter.swift in Sources */, @@ -1327,11 +1369,12 @@ 392221B4243D1627004D8794 /* RoomPowerLevelsEventView.swift in Sources */, CAF2AE92245AEEBA00D84133 /* BadgeView.swift in Sources */, CAC46D6323A278F40079C24F /* PreviewProvider+Enumeration.swift in Sources */, + 9525D2D82676548B004055B6 /* RecentRoomsView.swift in Sources */, 39D166C82385C832006DD257 /* EventContainerView.swift in Sources */, CAC46D6B23A55E940079C24F /* PeekableIterator.swift in Sources */, CAC46D5823A272D10079C24F /* MessageViewModel.swift in Sources */, 3921176224435E0000892B00 /* MediaEventView.swift in Sources */, - 3902B8A32395935600698B87 /* SettingsView.swift in Sources */, + 9525D2ED267659BD004055B6 /* SettingsContainerView.swift in Sources */, CAAF5BF82478696F006FDC33 /* UITextViewWrapper.swift in Sources */, 3921175F244256AF00892B00 /* Strings.swift in Sources */, 392221B0243A6F8E004D8794 /* EventContextMenuModel.swift in Sources */, @@ -1341,6 +1384,7 @@ 39BA0727240B534600FD28C6 /* Color+allAccent.swift in Sources */, CAD6817A246C31EB001878EB /* String+Extensions.swift in Sources */, CAC46D5D23A276700079C24F /* Color+Named.swift in Sources */, + 9525D2E426765850004055B6 /* RoomView.swift in Sources */, CAF2AE94245B507400D84133 /* Assets.swift in Sources */, 3902B89E2393FE8200698B87 /* GenericEventView.swift in Sources */, CAC46D6023A276B40079C24F /* IndividuallyRoundedRectangle.swift in Sources */, @@ -1396,8 +1440,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */, E843F31E25F2782E00B0F33B /* RoomMemberEventView.swift in Sources */, - E843F30C25F2781B00B0F33B /* RecentRoomsView.swift in Sources */, E843F30E25F2781F00B0F33B /* EventContextMenuModel.swift in Sources */, E843F30425F2781400B0F33B /* ReverseList.swift in Sources */, E843F32825F2783900B0F33B /* String+Extensions.swift in Sources */, @@ -1406,12 +1450,15 @@ E843F2FE25F2781400B0F33B /* AttributedText.swift in Sources */, E843F32325F2783200B0F33B /* AppIcon.swift in Sources */, E843F30525F2781400B0F33B /* SFSymbol.swift in Sources */, + 9525D2DB267654E9004055B6 /* RecentRoomsView.swift in Sources */, E843F32E25F2783D00B0F33B /* Strings.swift in Sources */, E843F32625F2783900B0F33B /* String+Emoji.swift in Sources */, E843F31925F2782B00B0F33B /* MessageView.swift in Sources */, E843F30925F2781800B0F33B /* MXURL.swift in Sources */, E843F31C25F2782E00B0F33B /* RedactionEventView.swift in Sources */, E843F2FB25F2780C00B0F33B /* LoginView.swift in Sources */, + 9525D2E526765850004055B6 /* RoomView.swift in Sources */, + 9525D2E226765808004055B6 /* RoomContainerView.swift in Sources */, E843F30F25F2782000B0F33B /* ReactionPicker.swift in Sources */, E843F30825F2781800B0F33B /* PeekableIterator.swift in Sources */, E843F2FC25F2780C00B0F33B /* LoadingView.swift in Sources */, @@ -1421,8 +1468,6 @@ E843F31F25F2782E00B0F33B /* RoomPowerLevelsEventView.swift in Sources */, E843F32F25F2783D00B0F33B /* Assets.swift in Sources */, E843F31225F2782000B0F33B /* MessageComposerView.swift in Sources */, - E843F2FF25F2781400B0F33B /* MessageTextViewWrapper.swift in Sources */, - E843F32425F2783200B0F33B /* SettingsView.swift in Sources */, E843F30D25F2781B00B0F33B /* RoomListItemView.swift in Sources */, E843F30625F2781800B0F33B /* TypedEvents.swift in Sources */, E843F30325F2781400B0F33B /* MarkdownText.swift in Sources */, @@ -1431,16 +1476,16 @@ E843F31425F2782700B0F33B /* GroupedReactionsView.swift in Sources */, E843F32725F2783900B0F33B /* Color+allAccent.swift in Sources */, E843F30A25F2781800B0F33B /* Formatter.swift in Sources */, - E843F30B25F2781B00B0F33B /* RoomView.swift in Sources */, E843F2F925F2780600B0F33B /* NioApp.swift in Sources */, E843F31625F2782700B0F33B /* ReactionGroupView.swift in Sources */, E843F32C25F2783900B0F33B /* EnvironmentValues.swift in Sources */, E843F32025F2782E00B0F33B /* RoomNameEventView.swift in Sources */, - E843F30225F2781400B0F33B /* ImagePicker.swift in Sources */, + 9525D2EC267659A2004055B6 /* SettingsContainerView.swift in Sources */, E843F31725F2782B00B0F33B /* BorderlessMessageView.swift in Sources */, E843F32925F2783900B0F33B /* NSAttributedString+Extensions.swift in Sources */, E843F32A25F2783900B0F33B /* Color+Named.swift in Sources */, E843F30725F2781800B0F33B /* GroupingIterator.swift in Sources */, + 9525D2E026765754004055B6 /* ImagePicker.swift in Sources */, E843F31825F2782B00B0F33B /* BorderedMessageView.swift in Sources */, E843F32B25F2783900B0F33B /* ContentSizeCategory.swift in Sources */, E843F31025F2782000B0F33B /* EventContextMenu.swift in Sources */, @@ -1448,6 +1493,7 @@ E843F32225F2782E00B0F33B /* BadgeView.swift in Sources */, E843F30025F2781400B0F33B /* MultilineTextField.swift in Sources */, E843F31B25F2782B00B0F33B /* MessageViewModel.swift in Sources */, + 9525D2DE26765700004055B6 /* MessageTextViewWrapper.swift in Sources */, E843F31325F2782200B0F33B /* EventContainerView.swift in Sources */, E843F32125F2782E00B0F33B /* RoomTopicEventView.swift in Sources */, ); diff --git a/Nio/Conversations/RecentRoomsContainerView.swift b/Nio/Conversations/RecentRoomsContainerView.swift new file mode 100644 index 00000000..0aeeb11f --- /dev/null +++ b/Nio/Conversations/RecentRoomsContainerView.swift @@ -0,0 +1,139 @@ +import SwiftUI +import MatrixSDK +import Introspect + +import NioKit + +struct RecentRoomsContainerView: View { + @EnvironmentObject var store: AccountStore + @AppStorage("accentColor") var accentColor: Color = .purple + + @State private var selectedNavigationItem: SelectedNavigationItem? + @State private var selectedRoomId: ObjectIdentifier? + + private func autoselectFirstRoom() { + if selectedRoomId == nil { + selectedRoomId = store.rooms.first?.id + } + } + + var body: some View { + RecentRoomsView(selectedNavigationItem: $selectedNavigationItem, + selectedRoomId: $selectedRoomId, + rooms: store.rooms) + .sheet(item: $selectedNavigationItem) { + NavigationSheet(selectedItem: $0, selectedRoomId: $selectedRoomId) + // This really shouldn't be necessary. SwiftUI bug? + // 2021-03-07(hh): SwiftUI doesn't document when + // environments are preserved. Also + // different between platforms. + .environmentObject(self.store) + .accentColor(accentColor) + } + .onAppear { + self.store.startListeningForRoomEvents() + if #available(macOS 11, *) { autoselectFirstRoom() } + } + } +} + + + +struct RoomsListSection: View { + let sectionHeader: String? + let rooms: [NIORoom] + let onLeaveAlertTitle: String + + @Binding var selectedRoomId: ObjectIdentifier? + + @State private var showConfirm: Bool = false + @State private var leaveId: Int? + + private var roomToLeave: NIORoom? { + guard + let leaveId = self.leaveId, + rooms.count > leaveId + else { return nil } + return self.rooms[leaveId] + } + + private var sectionContent: some View { + ForEach(rooms) { room in + NavigationLink(destination: RoomContainerView(room: room), tag: room.id, selection: $selectedRoomId) { + RoomListItemContainerView(room: room) + } + } + .onDelete(perform: setLeaveIndex) + } + + @ViewBuilder + private var section: some View { + if let sectionHeader = sectionHeader { + Section(header: Text(sectionHeader)) { + sectionContent + } + } else { + Section { + sectionContent + } + } + } + + var body: some View { + section + .alert(isPresented: $showConfirm) { + Alert( + title: Text(onLeaveAlertTitle), + message: Text(verbatim: L10n.RecentRooms.Leave.alertBody( + roomToLeave?.summary.displayname + ?? roomToLeave?.summary.roomId + ?? "")), + primaryButton: .destructive( + Text(verbatim: L10n.Room.Remove.action), + action: self.leaveRoom), + secondaryButton: .cancel()) + } + } + + private func setLeaveIndex(at offsets: IndexSet) { + self.showConfirm = true + for offset in offsets { + self.leaveId = offset + } + } + + private func leaveRoom() { + guard let leaveId = self.leaveId, rooms.count > leaveId else { return } + guard let mxRoom = self.roomToLeave?.room else { return } + mxRoom.mxSession?.leaveRoom(mxRoom.roomId) { _ in } + } +} + +enum SelectedNavigationItem: Int, Identifiable { + case settings + case newConversation + + var id: Int { + return self.rawValue + } +} + +struct NavigationSheet: View { + var selectedItem: SelectedNavigationItem + @Binding var selectedRoomId: ObjectIdentifier? + + var body: some View { + switch selectedItem { + case .settings: + SettingsContainerView() + case .newConversation: + NewConversationContainerView(createdRoomId: $selectedRoomId) + } + } +} + +/*struct RecentRoomsContainerView_Previews: PreviewProvider { + static var previews: some View { + RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), rooms: []) + } +}*/ diff --git a/Nio/Conversations/RecentRoomsView.swift b/Nio/Conversations/RecentRoomsView.swift index 141b5bdd..f22485f3 100644 --- a/Nio/Conversations/RecentRoomsView.swift +++ b/Nio/Conversations/RecentRoomsView.swift @@ -1,47 +1,21 @@ +// +// RecentRoomsView.swift +// Nio +// +// Created by Finn Behrens on 13.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + import SwiftUI import MatrixSDK -import Introspect import NioKit -struct RecentRoomsContainerView: View { - @EnvironmentObject var store: AccountStore - @AppStorage("accentColor") var accentColor: Color = .purple - - @State private var selectedNavigationItem: SelectedNavigationItem? - @State private var selectedRoomId: ObjectIdentifier? - - private func autoselectFirstRoom() { - if selectedRoomId == nil { - selectedRoomId = store.rooms.first?.id - } - } - - var body: some View { - RecentRoomsView(selectedNavigationItem: $selectedNavigationItem, - selectedRoomId: $selectedRoomId, - rooms: store.rooms) - .sheet(item: $selectedNavigationItem) { - NavigationSheet(selectedItem: $0, selectedRoomId: $selectedRoomId) - // This really shouldn't be necessary. SwiftUI bug? - // 2021-03-07(hh): SwiftUI doesn't document when - // environments are preserved. Also - // different between platforms. - .environmentObject(self.store) - .accentColor(accentColor) - } - .onAppear { - self.store.startListeningForRoomEvents() - if #available(macOS 11, *) { autoselectFirstRoom() } - } - } -} - struct RecentRoomsView: View { @EnvironmentObject var store: AccountStore - @Binding fileprivate var selectedNavigationItem: SelectedNavigationItem? - @Binding fileprivate var selectedRoomId: ObjectIdentifier? + @Binding var selectedNavigationItem: SelectedNavigationItem? + @Binding var selectedRoomId: ObjectIdentifier? let rooms: [NIORoom] @@ -53,41 +27,6 @@ struct RecentRoomsView: View { rooms.filter {$0.room.summary.membership == .invite} } - #if os(macOS) - var body: some View { - NavigationView { - List { - if !invitedRooms.isEmpty { - RoomsListSection( - sectionHeader: L10n.RecentRooms.PendingInvitations.header, - rooms: invitedRooms, - onLeaveAlertTitle: L10n.RecentRooms.PendingInvitations.Leave.alertTitle, - selectedRoomId: $selectedRoomId - ) - } - - RoomsListSection( - sectionHeader: invitedRooms.isEmpty ? nil : L10n.RecentRooms.Rooms.header , - rooms: joinedRooms, - onLeaveAlertTitle: L10n.RecentRooms.Leave.alertTitle, - selectedRoomId: $selectedRoomId - ) - - } - .listStyle(SidebarListStyle()) - .navigationTitle("Mio") - .frame(minWidth: Style.minSidebarWidth) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button(action: { self.selectedNavigationItem = .newConversation }) { - Label(L10n.RecentRooms.AccessibilityLabel.newConversation, - systemImage: SFSymbol.newConversation.rawValue) - } - } - } - } - } - #else // iOS private var settingsButton: some View { Button(action: { self.selectedNavigationItem = .settings @@ -141,101 +80,9 @@ struct RecentRoomsView: View { .navigationBarItems(leading: settingsButton, trailing: newConversationButton) } } - #endif // iOS -} - -struct RoomsListSection: View { - let sectionHeader: String? - let rooms: [NIORoom] - let onLeaveAlertTitle: String - - @Binding var selectedRoomId: ObjectIdentifier? - - @State private var showConfirm: Bool = false - @State private var leaveId: Int? - - private var roomToLeave: NIORoom? { - guard - let leaveId = self.leaveId, - rooms.count > leaveId - else { return nil } - return self.rooms[leaveId] - } - - private var sectionContent: some View { - ForEach(rooms) { room in - NavigationLink(destination: RoomContainerView(room: room), tag: room.id, selection: $selectedRoomId) { - RoomListItemContainerView(room: room) - } - } - .onDelete(perform: setLeaveIndex) - } - - @ViewBuilder - private var section: some View { - if let sectionHeader = sectionHeader { - Section(header: Text(sectionHeader)) { - sectionContent - } - } else { - Section { - sectionContent - } - } - } - - var body: some View { - section - .alert(isPresented: $showConfirm) { - Alert( - title: Text(onLeaveAlertTitle), - message: Text(verbatim: L10n.RecentRooms.Leave.alertBody( - roomToLeave?.summary.displayname - ?? roomToLeave?.summary.roomId - ?? "")), - primaryButton: .destructive( - Text(verbatim: L10n.Room.Remove.action), - action: self.leaveRoom), - secondaryButton: .cancel()) - } - } - - private func setLeaveIndex(at offsets: IndexSet) { - self.showConfirm = true - for offset in offsets { - self.leaveId = offset - } - } - - private func leaveRoom() { - guard let leaveId = self.leaveId, rooms.count > leaveId else { return } - guard let mxRoom = self.roomToLeave?.room else { return } - mxRoom.mxSession?.leaveRoom(mxRoom.roomId) { _ in } - } } -private enum SelectedNavigationItem: Int, Identifiable { - case settings - case newConversation - var id: Int { - return self.rawValue - } -} - -private struct NavigationSheet: View { - var selectedItem: SelectedNavigationItem - @Binding var selectedRoomId: ObjectIdentifier? - - var body: some View { - switch selectedItem { - case .settings: - SettingsContainerView() - case .newConversation: - NewConversationContainerView(createdRoomId: $selectedRoomId) - } - } -} struct RecentRoomsView_Previews: PreviewProvider { static var previews: some View { diff --git a/Nio/Conversations/RoomContainerView.swift b/Nio/Conversations/RoomContainerView.swift new file mode 100644 index 00000000..22be8a73 --- /dev/null +++ b/Nio/Conversations/RoomContainerView.swift @@ -0,0 +1,92 @@ +import SwiftUI +import Combine +import MatrixSDK + +import NioKit + +struct RoomContainerView: View { + @ObservedObject var room: NIORoom + + @State private var showAttachmentPicker = false + @State private var showImagePicker = false + @State private var eventToReactTo: String? + @State private var showJoinAlert = false + + private var roomView: RoomView { + RoomView( + events: room.events(), + isDirect: room.isDirect, + showAttachmentPicker: $showAttachmentPicker, + onCommit: { message in + self.room.send(text: message) + }, + onReact: { eventId in + self.eventToReactTo = eventId + }, + onRedact: { eventId, reason in + self.room.redact(eventId: eventId, reason: reason) + }, + onEdit: { message, eventId in + self.room.edit(text: message, eventId: eventId) + } + ) + } + + var body: some View { + roomView + .navigationBarTitle(Text(room.summary.displayname ?? ""), displayMode: .inline) + .actionSheet(isPresented: $showAttachmentPicker) { + self.attachmentPickerSheet + } + .sheet(item: $eventToReactTo) { eventId in + ReactionPicker { reaction in + self.room.react(toEventId: eventId, emoji: reaction) + self.eventToReactTo = nil + } + } + .alert(isPresented: $showJoinAlert) { + let roomName = self.room.summary.displayname ?? self.room.summary.roomId ?? L10n.Room.Invitation.fallbackTitle + return Alert( + title: Text(verbatim: L10n.Room.Invitation.JoinAlert.title), + message: Text(verbatim: L10n.Room.Invitation.JoinAlert.message(roomName)), + primaryButton: .default( + Text(verbatim: L10n.Room.Invitation.JoinAlert.joinButton), + action: { + self.room.room.mxSession.joinRoom(self.room.room.roomId) { _ in + self.room.markAllAsRead() + } + }), + secondaryButton: .cancel()) + } + .onAppear { + switch self.room.summary.membership { + case .invite: + self.showJoinAlert = true + case .join: + self.room.markAllAsRead() + default: + break + } + } + .environmentObject(room) + .background(EmptyView() + .sheet(isPresented: $showImagePicker) { + ImagePicker(sourceType: .photoLibrary) { image in + self.room.sendImage(image: image) + } + } + ) + } + + private var attachmentPickerSheet: ActionSheet { + ActionSheet( + title: Text(verbatim: L10n.Room.Attachment.selectType), buttons: [ + .default(Text(verbatim: L10n.Room.Attachment.typePhoto), action: { + self.showImagePicker = true + }), + .cancel() + ] + ) + } +} + diff --git a/Nio/Conversations/RoomView.swift b/Nio/Conversations/RoomView.swift index 3040699c..851d4ea0 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -4,128 +4,6 @@ import MatrixSDK import NioKit -struct RoomContainerView: View { - @ObservedObject var room: NIORoom - - @State private var showAttachmentPicker = false - @State private var showImagePicker = false - @State private var eventToReactTo: String? - @State private var showJoinAlert = false - - private var roomView: RoomView { - RoomView( - events: room.events(), - isDirect: room.isDirect, - showAttachmentPicker: $showAttachmentPicker, - onCommit: { message in - self.room.send(text: message) - }, - onReact: { eventId in - self.eventToReactTo = eventId - }, - onRedact: { eventId, reason in - self.room.redact(eventId: eventId, reason: reason) - }, - onEdit: { message, eventId in - self.room.edit(text: message, eventId: eventId) - } - ) - } - - #if os(macOS) - var body: some View { - VStack(spacing: 0) { - Divider() // TBD: This might be better done w/ toolbar styling - roomView - } - .navigationTitle(Text(room.summary.displayname ?? "")) - // TODO: action sheet - .sheet(item: $eventToReactTo) { eventId in - ReactionPicker { reaction in - self.room.react(toEventId: eventId, emoji: reaction) - self.eventToReactTo = nil - } - } - // TODO: join alert - .onAppear { - switch self.room.summary.membership { - case .invite: - self.showJoinAlert = true - case .join: - self.room.markAllAsRead() - default: - break - } - } - .environmentObject(room) - // TODO: background sheet thing - .background(Color(.textBackgroundColor)) - .frame(minWidth: Style.minTimelineWidth) - } - #else // iOS - var body: some View { - roomView - .navigationBarTitle(Text(room.summary.displayname ?? ""), displayMode: .inline) - .actionSheet(isPresented: $showAttachmentPicker) { - self.attachmentPickerSheet - } - .sheet(item: $eventToReactTo) { eventId in - ReactionPicker { reaction in - self.room.react(toEventId: eventId, emoji: reaction) - self.eventToReactTo = nil - } - } - .alert(isPresented: $showJoinAlert) { - let roomName = self.room.summary.displayname ?? self.room.summary.roomId ?? L10n.Room.Invitation.fallbackTitle - return Alert( - title: Text(verbatim: L10n.Room.Invitation.JoinAlert.title), - message: Text(verbatim: L10n.Room.Invitation.JoinAlert.message(roomName)), - primaryButton: .default( - Text(verbatim: L10n.Room.Invitation.JoinAlert.joinButton), - action: { - self.room.room.mxSession.joinRoom(self.room.room.roomId) { _ in - self.room.markAllAsRead() - } - }), - secondaryButton: .cancel()) - } - .onAppear { - switch self.room.summary.membership { - case .invite: - self.showJoinAlert = true - case .join: - self.room.markAllAsRead() - default: - break - } - } - .environmentObject(room) - .background(EmptyView() - .sheet(isPresented: $showImagePicker) { - ImagePicker(sourceType: .photoLibrary) { image in - self.room.sendImage(image: image) - } - } - ) - } - #endif // iOS - - #if os(macOS) - // TODO: port me to macOS - #else - private var attachmentPickerSheet: ActionSheet { - ActionSheet( - title: Text(verbatim: L10n.Room.Attachment.selectType), buttons: [ - .default(Text(verbatim: L10n.Room.Attachment.typePhoto), action: { - self.showImagePicker = true - }), - .cancel() - ] - ) - } - #endif -} - struct RoomView: View { @Environment(\.userId) private var userId @EnvironmentObject private var room: NIORoom @@ -149,7 +27,7 @@ struct RoomView: View { @State private var attributedMessage = NSAttributedString(string: "") @State private var shouldPaginate = false - + private var areOtherUsersTyping: Bool { return !(room.room.typingUsers?.filter { $0 != userId }.isEmpty ?? true) } @@ -177,15 +55,15 @@ struct RoomView: View { })) .padding(.horizontal) } - + if #available(macOS 11, *) { Divider() } - + if areOtherUsersTyping { TypingIndicatorContainerView() } - + MessageComposerView( showAttachmentPicker: $showAttachmentPicker, isEditing: $isEditingMessage, diff --git a/Nio/Settings/SettingsView.swift b/Nio/Settings/SettingsContainerView.swift similarity index 60% rename from Nio/Settings/SettingsView.swift rename to Nio/Settings/SettingsContainerView.swift index 19b20ac6..dfd13178 100644 --- a/Nio/Settings/SettingsView.swift +++ b/Nio/Settings/SettingsContainerView.swift @@ -1,55 +1,24 @@ +// +// SettingsContainerView.swift +// Nio +// +// Created by Finn Behrens on 13.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + import SwiftUI + import NioKit struct SettingsContainerView: View { @EnvironmentObject var store: AccountStore var body: some View { - #if os(macOS) - MacSettingsView(logoutAction: { - async { - await self.store.logout() - } - }) - #else - SettingsView(logoutAction: { + SettingsView(logoutAction: { async { await self.store.logout() } }) - #endif - } -} - -private struct MacSettingsView: View { - @AppStorage("accentColor") private var accentColor: Color = .purple - let logoutAction: () -> Void - - var body: some View { - Form { - Section { - Picker(selection: $accentColor, label: Text(verbatim: L10n.Settings.accentColor)) { - ForEach(Color.allAccentOptions, id: \.self) { color in - HStack { - Circle() - .frame(width: 20) - .foregroundColor(color) - Text(color.description.capitalized) - } - .tag(color) - } - } - // No icon picker on macOS - } - - Section { - Button(action: self.logoutAction) { - Text(verbatim: L10n.Settings.logOut) - } - } - } - .padding() - .frame(maxWidth: 320) } } @@ -99,8 +68,9 @@ private struct SettingsView: View { } } -struct SettingsView_Previews: PreviewProvider { + +struct SettingsContainerView_Previews: PreviewProvider { static var previews: some View { - SettingsView(logoutAction: {}) + SettingsContainerView() } } diff --git a/Nio/Shared Views/ImagePicker.swift b/Nio/Shared Views/ImagePicker.swift index 5203a338..2286cd8f 100644 --- a/Nio/Shared Views/ImagePicker.swift +++ b/Nio/Shared Views/ImagePicker.swift @@ -1,15 +1,5 @@ import SwiftUI -#if os(macOS) - -struct ImagePicker: View { - - var body: some View { - Text("Sorrz, no image picker on macOS yet :-/") - } -} - -#else // iOS struct ImagePicker: UIViewControllerRepresentable { @Environment(\.presentationMode) @@ -70,4 +60,3 @@ struct ImagePicker: UIViewControllerRepresentable { } } -#endif // iOS diff --git a/Nio/Shared Views/MessageTextViewWrapper.swift b/Nio/Shared Views/MessageTextViewWrapper.swift index 923e6faf..094296d1 100644 --- a/Nio/Shared Views/MessageTextViewWrapper.swift +++ b/Nio/Shared Views/MessageTextViewWrapper.swift @@ -1,47 +1,7 @@ import SwiftUI import NioKit -#if os(macOS) -class MessageTextView: NSTextView { - convenience init(attributedString: NSAttributedString, linkColor: UXColor, - maxSize: CGSize) - { - self.init() - backgroundColor = .clear - textContainerInset = .zero - isEditable = false - linkTextAttributes = [ - .foregroundColor: linkColor, - .underlineStyle: NSUnderlineStyle.single.rawValue, - ] - - self.insertText(attributedString, - replacementRange: NSRange(location: 0, length: 0)) - self.maxSize = maxSize - - // don't resist text wrapping across multiple lines - setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - } -} - -struct MessageTextViewWrapper: NSViewRepresentable { - let attributedString: NSAttributedString - let linkColor: NSColor - let maxSize: CGSize - - func makeNSView(context: Context) -> MessageTextView { - MessageTextView(attributedString: attributedString, linkColor: linkColor, maxSize: maxSize) - } - func updateNSView(_ uiView: MessageTextView, context: Context) { - // nothing to update - } - - func makeCoordinator() { - // nothing to coordinate - } -} -#else // iOS /// An automatically sized label, which allows links to be tapped. class MessageTextView: UITextView { var maxSize: CGSize = .zero @@ -92,4 +52,3 @@ struct MessageTextViewWrapper: UIViewRepresentable { // nothing to coordinate } } -#endif // iOS From 044d6875468e4c32713386ebfc2eaca5b0f5aae5 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Tue, 15 Jun 2021 18:18:15 +0200 Subject: [PATCH 03/11] fix pagination ciclyc dependency --- Nio.xcodeproj/project.pbxproj | 6 ++ Nio/Conversations/RoomView.swift | 43 ++++++---- Nio/NewConversation/NewConversationView.swift | 6 +- Nio/Shared Views/ReverseList.swift | 2 +- NioKit/Extensions/MX+Identifiable.swift | 2 +- NioKit/Extensions/MXAutoDiscovery+Async.swift | 8 +- NioKit/Extensions/MXClient+Publisher.swift | 10 +-- .../Extensions/MXCredentials+Keychain.swift | 12 +-- NioKit/Extensions/MXEvent+Extensions.swift | 14 ++-- NioKit/Extensions/MXEventTimeLine+Async.swift | 27 +++++++ NioKit/Extensions/MXRestClient+Async.swift | 14 ++-- NioKit/Extensions/MXRoom+Async.swift | 16 +++- NioKit/Extensions/UXKit.swift | 79 +++++++++---------- NioKit/Extensions/UserDefaults.swift | 15 ++-- NioKit/Models/NIORoom.swift | 56 +++++++++++++ NioKit/Session/AccountStore.swift | 49 ++++++------ 16 files changed, 232 insertions(+), 127 deletions(-) create mode 100644 NioKit/Extensions/MXEventTimeLine+Async.swift diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index e7fe73f3..5473490b 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ 9525D2EC267659A2004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */; }; 9525D2ED267659BD004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E62676594F004055B6 /* SettingsContainerView.swift */; }; 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; + 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; + 95CAC7B92678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */; }; A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */; }; A58352A625A667AB00533363 /* MatrixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A58352A525A667AB00533363 /* MatrixSDK */; }; @@ -383,6 +385,7 @@ 9525D2E326765850004055B6 /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; 9525D2E62676594F004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; + 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXEventTimeLine+Async.swift"; sourceTree = ""; }; A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NioApp.swift; sourceTree = ""; }; A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTextViewWrapper.swift; sourceTree = ""; }; CAAF5BF72478696F006FDC33 /* UITextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewWrapper.swift; sourceTree = ""; }; @@ -864,6 +867,7 @@ 393411C823904428003B49B8 /* MXEvent+Extensions.swift */, 4BEB8C03250403D200E90699 /* UserDefaults.swift */, E897AA3325F2716F00D11427 /* UXKit.swift */, + 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */, ); path = Extensions; sourceTree = ""; @@ -1427,6 +1431,7 @@ E8B4726125F26D5A00ACEFCB /* AccountStore.swift in Sources */, E8B4725825F26D5A00ACEFCB /* CustomEvent.swift in Sources */, E8B4725F25F26D5A00ACEFCB /* Configuration.swift in Sources */, + 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */, E8B4725D25F26D5A00ACEFCB /* EventCollection.swift in Sources */, 9525D2D426763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */, E8B4725E25F26D5A00ACEFCB /* MXEvent+Extensions.swift in Sources */, @@ -1516,6 +1521,7 @@ E897AA2325F2707C00D11427 /* Reaction.swift in Sources */, E897AA1925F2707C00D11427 /* EditEvent.swift in Sources */, E897AA1F25F2707C00D11427 /* MX+Identifiable.swift in Sources */, + 95CAC7B92678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */, E897AA1D25F2707C00D11427 /* MXCredentials+Keychain.swift in Sources */, 9525D2D326763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */, E897AA2425F2707C00D11427 /* MXEvent+Extensions.swift in Sources */, diff --git a/Nio/Conversations/RoomView.swift b/Nio/Conversations/RoomView.swift index 851d4ea0..0686dba8 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -1,6 +1,6 @@ -import SwiftUI import Combine import MatrixSDK +import SwiftUI import NioKit @@ -29,7 +29,7 @@ struct RoomView: View { @State private var shouldPaginate = false private var areOtherUsersTyping: Bool { - return !(room.room.typingUsers?.filter { $0 != userId }.isEmpty ?? true) + !(room.room.typingUsers?.filter { $0 != userId }.isEmpty ?? true) } var body: some View { @@ -41,18 +41,19 @@ struct RoomView: View { showSender: !self.isDirect, edits: self.events.relatedEvents(of: event).filter { $0.isEdit() }, contextMenuModel: EventContextMenuModel( - event: event, - userId: self.userId, - onReact: { self.onReact(event.eventId) }, - onReply: { }, - onEdit: { self.edit(event: event) }, - onRedact: { - if event.sentState == MXEventSentStateFailed { - room.removeOutgoingMessage(event) - } else { - self.eventToRedact = event.eventId - } - })) + event: event, + userId: self.userId, + onReact: { self.onReact(event.eventId) }, + onReply: {}, + onEdit: { self.edit(event: event) }, + onRedact: { + if event.sentState == MXEventSentStateFailed { + room.removeOutgoingMessage(event) + } else { + self.eventToRedact = event.eventId + } + } + )) .padding(.horizontal) } @@ -72,12 +73,20 @@ struct RoomView: View { onCancel: cancelEdit, onCommit: send ) - .padding(.horizontal) - .padding(.bottom, 10) + .padding(.horizontal) + .padding(.bottom, 10) } .onChange(of: shouldPaginate) { newValue in if newValue, let topEvent = events.renderableEvents.first { - store.paginate(room: self.room, event: topEvent) + asyncDetached { + print("paginating") + await room.paginate(topEvent) + } + } + } + .onAppear { + asyncDetached { + await room.createPagination() } } .alert(item: $eventToRedact) { eventId in diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 780117db..45c408df 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -70,7 +70,7 @@ private struct NewConversationView: View { // Seems to be a bug in Xcode, currently needs to be asnyc, to be able to await actor Button(action: { async { - createRoom + await createRoom() } }) { Text(verbatim: L10n.NewConversation.createRoom) @@ -151,7 +151,7 @@ private struct NewConversationView: View { } } - @MainActor private func createRoom() { + private func createRoom() async { isWaiting = true let parameters = MXRoomCreationParameters() @@ -173,7 +173,7 @@ private struct NewConversationView: View { } } - store?.session?.createRoom(parameters: parameters) { response in + await store?.session?.createRoom(parameters: parameters) { response in switch response { case .success(let room): createdRoomId = room.id diff --git a/Nio/Shared Views/ReverseList.swift b/Nio/Shared Views/ReverseList.swift index 503a3eaf..4547ed63 100644 --- a/Nio/Shared Views/ReverseList.swift +++ b/Nio/Shared Views/ReverseList.swift @@ -43,7 +43,7 @@ struct ReverseList: View where Element: Identifiable, Content: } .frame(height: 30) // FIXME: Frame height shouldn't be hard-coded .onPreferenceChange(IsVisibleKey.self) { - hasReachedTop = $0 + if $0 != hasReachedTop { hasReachedTop = $0 } } } .scaleEffect(x: -1.0, y: 1.0) diff --git a/NioKit/Extensions/MX+Identifiable.swift b/NioKit/Extensions/MX+Identifiable.swift index 1fce4e0e..fa7a97ba 100644 --- a/NioKit/Extensions/MX+Identifiable.swift +++ b/NioKit/Extensions/MX+Identifiable.swift @@ -1,5 +1,5 @@ -import SwiftUI import MatrixSDK +import SwiftUI extension MXPublicRoom: Identifiable {} extension MXRoom: Identifiable {} diff --git a/NioKit/Extensions/MXAutoDiscovery+Async.swift b/NioKit/Extensions/MXAutoDiscovery+Async.swift index 9129eca5..8850a922 100644 --- a/NioKit/Extensions/MXAutoDiscovery+Async.swift +++ b/NioKit/Extensions/MXAutoDiscovery+Async.swift @@ -8,10 +8,10 @@ import Foundation import MatrixSDK -extension MXAutoDiscovery { - public func findClientConfig() async throws -> MXDiscoveredClientConfig { - return try await withCheckedThrowingContinuation {continuation in - self.findClientConfig({ continuation.resume(returning: $0)}, failure: {continuation.resume(throwing: $0)}) +public extension MXAutoDiscovery { + func findClientConfig() async throws -> MXDiscoveredClientConfig { + try await withCheckedThrowingContinuation { continuation in + self.findClientConfig({ continuation.resume(returning: $0) }, failure: { continuation.resume(throwing: $0) }) } } } diff --git a/NioKit/Extensions/MXClient+Publisher.swift b/NioKit/Extensions/MXClient+Publisher.swift index 1b5ab5c9..e5c7e2c6 100644 --- a/NioKit/Extensions/MXClient+Publisher.swift +++ b/NioKit/Extensions/MXClient+Publisher.swift @@ -1,15 +1,15 @@ -import Foundation import Combine +import Foundation import MatrixSDK -extension MXRestClient { - public func nio_publicRooms(onServer: String? = nil, limit: UInt? = nil) -> AnyPublisher { +public extension MXRestClient { + func nio_publicRooms(onServer: String? = nil, limit: UInt? = nil) -> AnyPublisher { Future { promise in self.publicRooms(onServer: onServer, limit: limit) { response in switch response { - case .failure(let error): + case let .failure(error): promise(.failure(error)) - case .success(let publicRoomsResponse): + case let .success(publicRoomsResponse): promise(.success(publicRoomsResponse)) @unknown default: fatalError("Unexpected Matrix response: \(response)") diff --git a/NioKit/Extensions/MXCredentials+Keychain.swift b/NioKit/Extensions/MXCredentials+Keychain.swift index 9bdbc0b3..e2dbf727 100644 --- a/NioKit/Extensions/MXCredentials+Keychain.swift +++ b/NioKit/Extensions/MXCredentials+Keychain.swift @@ -1,10 +1,10 @@ -import MatrixSDK import KeychainAccess +import MatrixSDK -extension MXCredentials { - public func save(to keychain: Keychain) { +public extension MXCredentials { + func save(to keychain: Keychain) { guard - let homeserver = self.homeServer, + let homeserver = homeServer, let userId = self.userId, let accessToken = self.accessToken, let deviceId = self.deviceId @@ -17,14 +17,14 @@ extension MXCredentials { keychain["deviceId"] = deviceId } - public func clear(from keychain: Keychain) { + func clear(from keychain: Keychain) { keychain["homeserver"] = nil keychain["userId"] = nil keychain["accessToken"] = nil keychain["deviceId"] = nil } - public static func from(_ keychain: Keychain) -> MXCredentials? { + static func from(_ keychain: Keychain) -> MXCredentials? { guard let homeserver = keychain["homeserver"], let userId = keychain["userId"], diff --git a/NioKit/Extensions/MXEvent+Extensions.swift b/NioKit/Extensions/MXEvent+Extensions.swift index fc34cc89..fbe79476 100644 --- a/NioKit/Extensions/MXEvent+Extensions.swift +++ b/NioKit/Extensions/MXEvent+Extensions.swift @@ -1,20 +1,20 @@ import Foundation import MatrixSDK -extension MXEvent { - public var timestamp: Date { - Date(timeIntervalSince1970: TimeInterval(self.originServerTs / 1000)) +public extension MXEvent { + var timestamp: Date { + Date(timeIntervalSince1970: TimeInterval(originServerTs / 1000)) } - public func content(valueFor key: String) -> T? { - if let value = self.content?[key] as? T { + func content(valueFor key: String) -> T? { + if let value = content?[key] as? T { return value } return nil } - public func prevContent(valueFor key: String) -> T? { - if let value = self.unsignedData?.prevContent?[key] as? T { + func prevContent(valueFor key: String) -> T? { + if let value = unsignedData?.prevContent?[key] as? T { return value } return nil diff --git a/NioKit/Extensions/MXEventTimeLine+Async.swift b/NioKit/Extensions/MXEventTimeLine+Async.swift new file mode 100644 index 00000000..a124e864 --- /dev/null +++ b/NioKit/Extensions/MXEventTimeLine+Async.swift @@ -0,0 +1,27 @@ +// +// MXEventTimeLine+Async.swift +// Nio +// +// Created by Finn Behrens on 15.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Foundation +import MatrixSDK + +extension MXEventTimeline { + func paginate(_ numItems: UInt, direction: MXTimelineDirection = .backwards, onlyFromStore: Bool = false) async throws { + return try await withCheckedThrowingContinuation {continuation in + self.paginate(numItems, direction: direction, onlyFromStore: onlyFromStore, completion: {resp in + switch resp { + case .success(_): + continuation.resume() + case .failure(let e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } +} diff --git a/NioKit/Extensions/MXRestClient+Async.swift b/NioKit/Extensions/MXRestClient+Async.swift index c14d8679..fda623da 100644 --- a/NioKit/Extensions/MXRestClient+Async.swift +++ b/NioKit/Extensions/MXRestClient+Async.swift @@ -10,12 +10,12 @@ import MatrixSDK extension MXRestClient { func login(type loginType: MatrixSDK.MXLoginFlowType = .password, username: String, password: String) async throws -> MXCredentials { - return try await withCheckedThrowingContinuation {continuation in - self.login(type: loginType, username: username, password: password, completion: {resp in + try await withCheckedThrowingContinuation { continuation in + self.login(type: loginType, username: username, password: password, completion: { resp in switch resp { - case .success(let v): + case let .success(v): continuation.resume(returning: v) - case .failure(let e): + case let .failure(e): continuation.resume(throwing: e) @unknown default: continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) @@ -23,10 +23,10 @@ extension MXRestClient { }) } } - + func wellKnown() async throws -> MXWellKnown { - return try await withCheckedThrowingContinuation {continuation in - self.wellKnow({continuation.resume(returning: $0!)}, failure: {continuation.resume(throwing: $0!)}) + try await withCheckedThrowingContinuation { continuation in + self.wellKnow({ continuation.resume(returning: $0!) }, failure: { continuation.resume(throwing: $0!) }) } } } diff --git a/NioKit/Extensions/MXRoom+Async.swift b/NioKit/Extensions/MXRoom+Async.swift index 89bff57b..1f1732e5 100644 --- a/NioKit/Extensions/MXRoom+Async.swift +++ b/NioKit/Extensions/MXRoom+Async.swift @@ -10,12 +10,12 @@ import MatrixSDK extension MXRoom { func members() async throws -> MXRoomMembers? { - return try await withCheckedThrowingContinuation {continuation in - self.members(completion: {resp in + try await withCheckedThrowingContinuation { continuation in + self.members(completion: { resp in switch resp { - case .success(let v): + case let .success(v): continuation.resume(returning: v) - case .failure(let e): + case let .failure(e): continuation.resume(throwing: e) @unknown default: continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) @@ -23,4 +23,12 @@ extension MXRoom { }) } } + + var liveTimeline: MXEventTimeline { + get async { + await withCheckedContinuation { continuation in + self.liveTimeline { continuation.resume(returning: $0!) } + } + } + } } diff --git a/NioKit/Extensions/UXKit.swift b/NioKit/Extensions/UXKit.swift index dcf0d98b..95bb7035 100644 --- a/NioKit/Extensions/UXKit.swift +++ b/NioKit/Extensions/UXKit.swift @@ -9,67 +9,64 @@ #if os(macOS) import AppKit - public typealias UXColor = NSColor - public typealias UXImage = NSImage + public typealias UXColor = NSColor + public typealias UXImage = NSImage public typealias UXEdgeInsets = NSEdgeInsets - public typealias UXFont = NSFont + public typealias UXFont = NSFont public enum UXFakeTraitCollection { case current } public extension NSColor { - @inlinable - func resolvedColor(with fakeTraitCollection: UXFakeTraitCollection) - -> UXColor + func resolvedColor(with _: UXFakeTraitCollection) + -> UXColor { - return self + self } } - #if canImport(SwiftUI) - import SwiftUI - - public enum UXFakeDisplayMode { - case inline, automatic, large - } - public enum UXFakeAutocapitalizationMode { - case none - } + #if canImport(SwiftUI) + import SwiftUI - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) - public extension View { - - @inlinable - func navigationBarTitle( - _ title: S, displayMode: UXFakeDisplayMode = .inline - ) -> some View - { - self.navigationTitle(title) + public enum UXFakeDisplayMode { + case inline, automatic, large } - - @inlinable - func autocapitalization(_ mode: UXFakeAutocapitalizationMode) -> Self { - return self + + public enum UXFakeAutocapitalizationMode { + case none } - } - #endif + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public extension View { + @inlinable + func navigationBarTitle( + _ title: S, displayMode _: UXFakeDisplayMode = .inline + ) -> some View { + navigationTitle(title) + } + + @inlinable + func autocapitalization(_: UXFakeAutocapitalizationMode) -> Self { + self + } + } + #endif #elseif canImport(UIKit) import UIKit - public typealias UXColor = UIColor - public typealias UXImage = UIImage + public typealias UXColor = UIColor + public typealias UXImage = UIImage public typealias UXEdgeInsets = UIEdgeInsets - public typealias UXFont = UIFont + public typealias UXFont = UIFont - #if canImport(SwiftUI) - import SwiftUI - - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) - public extension View { - } - #endif + #if canImport(SwiftUI) + import SwiftUI + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public extension View {} + #endif #else #error("GNUstep not yet supported, sorry!") #endif diff --git a/NioKit/Extensions/UserDefaults.swift b/NioKit/Extensions/UserDefaults.swift index d4c689ae..6d19b469 100644 --- a/NioKit/Extensions/UserDefaults.swift +++ b/NioKit/Extensions/UserDefaults.swift @@ -15,14 +15,15 @@ public extension UserDefaults { } return group }() - #if os(macOS) - private static let teamIdentifierPrefix = Bundle.main - .object(forInfoDictionaryKey: "TeamIdentifierPrefix") as? String ?? "" - private static let suiteName = teamIdentifierPrefix + appGroup - #else // iOS - private static let suiteName = "group." + appGroup - #endif + #if os(macOS) + private static let teamIdentifierPrefix = Bundle.main + .object(forInfoDictionaryKey: "TeamIdentifierPrefix") as? String ?? "" + + private static let suiteName = teamIdentifierPrefix + appGroup + #else // iOS + private static let suiteName = "group." + appGroup + #endif static let group = UserDefaults(suiteName: suiteName)! } diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index 3c299481..43d0734e 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -3,6 +3,8 @@ import Combine import MatrixSDK +import os + public struct RoomItem: Codable, Hashable { public static func == (lhs: RoomItem, rhs: RoomItem) -> Bool { return lhs.displayName == rhs.displayName && @@ -22,6 +24,8 @@ public struct RoomItem: Codable, Hashable { @MainActor public class NIORoom: ObservableObject { + static let logger = Logger(subsystem: "chat.nio", category: "ROOM") + public let room: MXRoom @Published @@ -172,6 +176,58 @@ public class NIORoom: ObservableObject { objectWillChange.send() // room.outgoingMessages() will change room.removeOutgoingMessage(event.eventId) } + + //private var lastPaginatedEvent: MXEvent? + private var timeline: MXEventTimeline? + + public func paginate(_ event: MXEvent, direction: MXTimelineDirection = .backwards, numItems: UInt = 40) async { + /*guard event != lastPaginatedEvent else { + return + }*/ + + if timeline == nil { + Self.logger.debug("creating timeline for room '\(self.displayName)' with event '\(event.eventId)'") + //lastPaginatedEvent = event + timeline = room.timeline(onEvent: event.eventId) + let _ = timeline?.listenToEvents { + event, direction, roomState in + if direction == .backwards { + // eventCache is published, so no objectWillChanges.send here + self.add(event: event, direction: direction, roomState: roomState) + } + } + timeline?.resetPagination() + } + + if timeline?.canPaginate(direction) ?? false { + do { + try await timeline?.paginate(numItems, direction: direction, onlyFromStore: false) + } catch { + Self.logger.warning("could not paginate: \(error.localizedDescription)") + } + } else { + Self.logger.debug("cannot paginate: \(self.displayName)") + } + } + + public func createPagination() async { + guard timeline == nil else { + return + } + Self.logger.debug("Bootstraping pagination") + + timeline = await room.liveTimeline + timeline?.resetPagination() + if timeline?.canPaginate(.backwards) ?? false { + do { + try await timeline?.paginate(40, direction: .backwards) + } catch { + Self.logger.warning("could not bootstrap pagination: \(error.localizedDescription)") + } + } else { + Self.logger.warning("could not bootstrap pagination") + } + } } extension NIORoom: Identifiable { diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 781fd4e9..49e6b905 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -1,7 +1,7 @@ -import Foundation import Combine -import MatrixSDK +import Foundation import KeychainAccess +import MatrixSDK public enum LoginState { case loggedOut @@ -20,7 +20,8 @@ public class AccountStore: ObservableObject { let keychain = Keychain( service: "chat.nio.credentials", - accessGroup: ((Bundle.main.infoDictionary?["DevelopmentTeam"] as? String) ?? "") + ".nio.keychain") + accessGroup: ((Bundle.main.infoDictionary?["DevelopmentTeam"] as? String) ?? "") + ".nio.keychain" + ) public init() { if CommandLine.arguments.contains("-clear-stored-credentials") { @@ -35,7 +36,7 @@ public class AccountStore: ObservableObject { return } self.credentials = credentials - self.loginState = .authenticating + loginState = .authenticating async { do { self.loginState = try await self.sync() @@ -56,35 +57,35 @@ public class AccountStore: ObservableObject { @Published public var loginState: LoginState = .loggedOut public func login(username: String, password: String, homeserver: URL) async { - self.loginState = .authenticating + loginState = .authenticating - self.client = MXRestClient(homeServer: homeserver, unrecognizedCertificateHandler: nil) - self.client?.acceptableContentTypes = ["text/plain", "text/html", "application/json", "application/octet-stream", "any"] + client = MXRestClient(homeServer: homeserver, unrecognizedCertificateHandler: nil) + client?.acceptableContentTypes = ["text/plain", "text/html", "application/json", "application/octet-stream", "any"] do { - let credentials = try await self.client?.login(username: username, password: password) + let credentials = try await client?.login(username: username, password: password) guard let credentials = credentials else { - self.loginState = .failure(AccountStoreError.noCredentials) + loginState = .failure(AccountStoreError.noCredentials) return } self.credentials = credentials - credentials.save(to: self.keychain) - self.loginState = try await self.sync() - self.session?.crypto.warnOnUnknowDevices = false + credentials.save(to: keychain) + loginState = try await sync() + session?.crypto.warnOnUnknowDevices = false } catch { - self.loginState = .failure(error) + loginState = .failure(error) } } public func logout() async { - self.credentials?.clear(from: keychain) + credentials?.clear(from: keychain) do { - try await self.session?.logout() - self.loginState = .loggedOut + try await session?.logout() + loginState = .loggedOut } catch { // Close the session even if the logout request failed - self.loginState = .loggedOut + loginState = .loggedOut } } @@ -100,18 +101,17 @@ public class AccountStore: ObservableObject { } } - private func sync() async throws -> LoginState { guard let credentials = self.credentials else { throw AccountStoreError.noCredentials } - self.client = MXRestClient(credentials: credentials, unrecognizedCertificateHandler: nil) - self.session = MXSession(matrixRestClient: self.client!) - self.fileStore = MXFileStore() + client = MXRestClient(credentials: credentials, unrecognizedCertificateHandler: nil) + session = MXSession(matrixRestClient: client!) + fileStore = MXFileStore() - try await self.session!.setStore(fileStore!) - try await self.session?.start() + try await session!.setStore(fileStore!) + try await session?.start() return .loggedIn(userId: credentials.userId!) } @@ -121,7 +121,7 @@ public class AccountStore: ObservableObject { public func startListeningForRoomEvents() { // roomState is nil for presence events, just for future reference - listenReference = self.session?.listenToEvents { event, direction, roomState in + listenReference = session?.listenToEvents { event, direction, roomState in let affectedRooms = self.rooms.filter { $0.summary.roomId == event.roomId } for room in affectedRooms { room.add(event: event, direction: direction, roomState: roomState as? MXRoomState) @@ -161,6 +161,7 @@ public class AccountStore: ObservableObject { var listenReferenceRoom: Any? + @available(*, deprecated, message: "Prefer paginating on the room instead") public func paginate(room: NIORoom, event: MXEvent) { let timeline = room.room.timeline(onEvent: event.eventId) listenReferenceRoom = timeline?.listenToEvents { event, direction, roomState in From 8cc704ade730938cb7bbea918d14207804fd086b Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Thu, 17 Jun 2021 11:14:39 +0200 Subject: [PATCH 04/11] Developer settings --- Configs/GlobalConfig.xcconfig | 16 +- Mio/Conversations/RecentRoomsView.swift | 2 +- Mio/Conversations/RoomContainerView.swift | 4 +- Nio.xcodeproj/project.pbxproj | 322 +++++++++++++++++- .../xcschemes/Nio-nolaunch.xcscheme | 78 +++++ .../xcshareddata/xcschemes/Nio.xcscheme | 4 + .../xcschemes/NioIntentsExtension.xcscheme | 103 ++++++ .../xcschemes/NioIntentsExtensionUI.xcscheme | 97 ++++++ .../xcschemes/NioShareExtension.xcscheme | 97 ++++++ Nio/AppDelegate.swift | 81 +++++ .../RecentRoomsContainerView.swift | 25 +- Nio/Conversations/RecentRoomsView.swift | 2 +- Nio/Conversations/RoomContainerView.swift | 8 +- Nio/Conversations/RoomView.swift | 23 ++ Nio/Info.plist | 20 +- Nio/NewConversation/NewConversationView.swift | 8 +- Nio/Nio.entitlements | 6 + Nio/NioApp.swift | 28 ++ Nio/RootView.swift | 6 +- Nio/Settings/SettingsContainerView.swift | 48 +++ NioIntentsExtension/Info.plist | 36 ++ NioIntentsExtension/IntentHandler.swift | 125 +++++++ NioIntentsExtension/Intents.intentdefinition | 299 ++++++++++++++++ .../NioIntentsExtension.entitlements | 10 + .../Base.lproj/MainInterface.storyboard | 26 ++ NioIntentsExtensionUI/Info.plist | 24 ++ .../IntentViewController.swift | 37 ++ .../NioIntentsExtensionUI.entitlements | 10 + NioKit/Extensions/MX+Identifiable.swift | 61 +++- NioKit/Extensions/MXRoom+Async.swift | 32 ++ NioKit/Extensions/MXRoomMember+INPerson.swift | 36 ++ NioKit/Models/NIORoom.swift | 100 +++++- NioKit/Session/AccountStore.swift | 30 ++ NioShareExtension/ShareViewController.swift | 6 +- 34 files changed, 1763 insertions(+), 47 deletions(-) create mode 100644 Nio.xcodeproj/xcshareddata/xcschemes/Nio-nolaunch.xcscheme create mode 100644 Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtension.xcscheme create mode 100644 Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtensionUI.xcscheme create mode 100644 Nio.xcodeproj/xcshareddata/xcschemes/NioShareExtension.xcscheme create mode 100644 Nio/AppDelegate.swift create mode 100644 NioIntentsExtension/Info.plist create mode 100644 NioIntentsExtension/IntentHandler.swift create mode 100644 NioIntentsExtension/Intents.intentdefinition create mode 100644 NioIntentsExtension/NioIntentsExtension.entitlements create mode 100644 NioIntentsExtensionUI/Base.lproj/MainInterface.storyboard create mode 100644 NioIntentsExtensionUI/Info.plist create mode 100644 NioIntentsExtensionUI/IntentViewController.swift create mode 100644 NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements create mode 100644 NioKit/Extensions/MXRoomMember+INPerson.swift diff --git a/Configs/GlobalConfig.xcconfig b/Configs/GlobalConfig.xcconfig index 83152481..e6e55fb7 100644 --- a/Configs/GlobalConfig.xcconfig +++ b/Configs/GlobalConfig.xcconfig @@ -8,13 +8,15 @@ // NIO_NAMESPACE = com.example.nio // DEVELOPMENT_TEAM = Z123456789 -NIO_NAMESPACE = com.example.nio -DEVELOPMENT_TEAM = Z123456789 +NIO_NAMESPACE = com.example.nio +DEVELOPMENT_TEAM = Z123456789 -APPGROUP = $(NIO_NAMESPACE) -PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS -MAC_PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).mio -SHARE_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioShareExtension -CODE_SIGN_STYLE = Manual +APPGROUP = $(NIO_NAMESPACE) +PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS +MAC_PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).mio +SHARE_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioShareExtension +INTENTS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioIntents +INTENTSUI_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioIntentsUI +CODE_SIGN_STYLE = Manual #include? "LocalConfig.xcconfig" diff --git a/Mio/Conversations/RecentRoomsView.swift b/Mio/Conversations/RecentRoomsView.swift index c89c7325..3cbde163 100644 --- a/Mio/Conversations/RecentRoomsView.swift +++ b/Mio/Conversations/RecentRoomsView.swift @@ -15,7 +15,7 @@ struct RecentRoomsView: View { @EnvironmentObject var store: AccountStore @Binding var selectedNavigationItem: SelectedNavigationItem? - @Binding var selectedRoomId: ObjectIdentifier? + @Binding var selectedRoomId: MXRoom.MXRoomId? let rooms: [NIORoom] diff --git a/Mio/Conversations/RoomContainerView.swift b/Mio/Conversations/RoomContainerView.swift index 9ee31695..04784822 100644 --- a/Mio/Conversations/RoomContainerView.swift +++ b/Mio/Conversations/RoomContainerView.swift @@ -18,7 +18,9 @@ struct RoomContainerView: View { isDirect: room.isDirect, showAttachmentPicker: $showAttachmentPicker, onCommit: { message in - self.room.send(text: message) + asyncDetached { + await self.room.send(text: message) + } }, onReact: { eventId in self.eventToReactTo = eventId diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 5473490b..6a13a126 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -81,6 +81,17 @@ 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; 95CAC7B92678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; + 95CAC86926791295001D71DC /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95CAC86826791295001D71DC /* Intents.framework */; }; + 95CAC86C26791295001D71DC /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC86B26791295001D71DC /* IntentHandler.swift */; }; + 95CAC87426791295001D71DC /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95CAC87326791295001D71DC /* IntentsUI.framework */; }; + 95CAC87726791295001D71DC /* IntentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC87626791295001D71DC /* IntentViewController.swift */; }; + 95CAC87A26791295001D71DC /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95CAC87826791295001D71DC /* MainInterface.storyboard */; }; + 95CAC87E26791295001D71DC /* NioIntentsExtensionUI.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 95CAC88226791295001D71DC /* NioIntentsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 95CAC86726791295001D71DC /* NioIntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 95CAC88B26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */; }; + 95CAC88C26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */; }; + 95CAC88E26791D11001D71DC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88D26791D11001D71DC /* AppDelegate.swift */; }; + 95CAC89026792247001D71DC /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88F26792247001D71DC /* Intents.intentdefinition */; }; A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */; }; A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */; }; A58352A625A667AB00533363 /* MatrixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A58352A525A667AB00533363 /* MatrixSDK */; }; @@ -219,6 +230,20 @@ remoteGlobalIDString = 4BFEFD7B246F414D00CCF4A0; remoteInfo = NioShareExtension; }; + 95CAC87C26791295001D71DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39C931D123843289004449E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 95CAC87126791295001D71DC; + remoteInfo = NioIntentsExtensionUI; + }; + 95CAC88026791295001D71DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39C931D123843289004449E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 95CAC86626791295001D71DC; + remoteInfo = NioIntentsExtension; + }; CADF663924614D2300F5063F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 39C931D123843289004449E1 /* Project object */; @@ -257,6 +282,8 @@ dstSubfolderSpec = 13; files = ( 4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */, + 95CAC87E26791295001D71DC /* NioIntentsExtensionUI.appex in Embed App Extensions */, + 95CAC88226791295001D71DC /* NioIntentsExtension.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -386,6 +413,20 @@ 9525D2E62676594F004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXEventTimeLine+Async.swift"; sourceTree = ""; }; + 95CAC86726791295001D71DC /* NioIntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NioIntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 95CAC86826791295001D71DC /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + 95CAC86B26791295001D71DC /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 95CAC86D26791295001D71DC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NioIntentsExtensionUI.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 95CAC87326791295001D71DC /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + 95CAC87626791295001D71DC /* IntentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentViewController.swift; sourceTree = ""; }; + 95CAC87926791295001D71DC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 95CAC87B26791295001D71DC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 95CAC87F26791295001D71DC /* NioIntentsExtensionUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioIntentsExtensionUI.entitlements; sourceTree = ""; }; + 95CAC88326791295001D71DC /* NioIntentsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioIntentsExtension.entitlements; sourceTree = ""; }; + 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRoomMember+INPerson.swift"; sourceTree = ""; }; + 95CAC88D26791D11001D71DC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 95CAC88F26792247001D71DC /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; }; A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NioApp.swift; sourceTree = ""; }; A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTextViewWrapper.swift; sourceTree = ""; }; CAAF5BF72478696F006FDC33 /* UITextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewWrapper.swift; sourceTree = ""; }; @@ -454,6 +495,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95CAC86426791295001D71DC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC86926791295001D71DC /* Intents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 95CAC86F26791295001D71DC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC87426791295001D71DC /* IntentsUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CADF663224614D2300F5063F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -651,6 +708,8 @@ CADF663B24614D2300F5063F /* NioKitTests */, 4BFEFD7D246F414D00CCF4A0 /* NioShareExtension */, E843F2DB25F2748200B0F33B /* Mio */, + 95CAC86A26791295001D71DC /* NioIntentsExtension */, + 95CAC87526791295001D71DC /* NioIntentsExtensionUI */, 39C931DA2384328A004449E1 /* Products */, A58352AA25A667B300533363 /* Frameworks */, ); @@ -667,6 +726,8 @@ E8302D4C25F26CA500E962E9 /* libNioKit-iOS.a */, E897AA1525F2706D00D11427 /* libNioKit-macOS.a */, E843F2DA25F2748200B0F33B /* Mio.app */, + 95CAC86726791295001D71DC /* NioIntentsExtension.appex */, + 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */, ); name = Products; sourceTree = ""; @@ -677,6 +738,7 @@ A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */, 4BFEFD90246F5EF500CCF4A0 /* Nio.entitlements */, 39C931E02384328A004449E1 /* RootView.swift */, + 95CAC88D26791D11001D71DC /* AppDelegate.swift */, 3902B8A62395A78100698B87 /* Authentication */, CAC46D5E23A276B30079C24F /* Shapes */, 392389CA238EBB0800B2E1DF /* Shared Views */, @@ -826,9 +888,33 @@ path = Settings; sourceTree = ""; }; + 95CAC86A26791295001D71DC /* NioIntentsExtension */ = { + isa = PBXGroup; + children = ( + 95CAC88326791295001D71DC /* NioIntentsExtension.entitlements */, + 95CAC86B26791295001D71DC /* IntentHandler.swift */, + 95CAC86D26791295001D71DC /* Info.plist */, + 95CAC88F26792247001D71DC /* Intents.intentdefinition */, + ); + path = NioIntentsExtension; + sourceTree = ""; + }; + 95CAC87526791295001D71DC /* NioIntentsExtensionUI */ = { + isa = PBXGroup; + children = ( + 95CAC87F26791295001D71DC /* NioIntentsExtensionUI.entitlements */, + 95CAC87626791295001D71DC /* IntentViewController.swift */, + 95CAC87826791295001D71DC /* MainInterface.storyboard */, + 95CAC87B26791295001D71DC /* Info.plist */, + ); + path = NioIntentsExtensionUI; + sourceTree = ""; + }; A58352AA25A667B300533363 /* Frameworks */ = { isa = PBXGroup; children = ( + 95CAC86826791295001D71DC /* Intents.framework */, + 95CAC87326791295001D71DC /* IntentsUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -868,6 +954,7 @@ 4BEB8C03250403D200E90699 /* UserDefaults.swift */, E897AA3325F2716F00D11427 /* UXKit.swift */, 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */, + 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */, ); path = Extensions; sourceTree = ""; @@ -987,6 +1074,8 @@ dependencies = ( E8B4726925F26DCB00ACEFCB /* PBXTargetDependency */, 4BFEFD85246F414D00CCF4A0 /* PBXTargetDependency */, + 95CAC87D26791295001D71DC /* PBXTargetDependency */, + 95CAC88126791295001D71DC /* PBXTargetDependency */, ); name = Nio; packageProductDependencies = ( @@ -1018,6 +1107,40 @@ productReference = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 95CAC86626791295001D71DC /* NioIntentsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 95CAC88926791295001D71DC /* Build configuration list for PBXNativeTarget "NioIntentsExtension" */; + buildPhases = ( + 95CAC86326791295001D71DC /* Sources */, + 95CAC86426791295001D71DC /* Frameworks */, + 95CAC86526791295001D71DC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NioIntentsExtension; + productName = NioIntentsExtension; + productReference = 95CAC86726791295001D71DC /* NioIntentsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 95CAC87126791295001D71DC /* NioIntentsExtensionUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 95CAC88826791295001D71DC /* Build configuration list for PBXNativeTarget "NioIntentsExtensionUI" */; + buildPhases = ( + 95CAC86E26791295001D71DC /* Sources */, + 95CAC86F26791295001D71DC /* Frameworks */, + 95CAC87026791295001D71DC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NioIntentsExtensionUI; + productName = NioIntentsExtensionUI; + productReference = 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */; + productType = "com.apple.product-type.app-extension"; + }; CADF663424614D2300F5063F /* NioKitTests */ = { isa = PBXNativeTarget; buildConfigurationList = CADF664824614D2300F5063F /* Build configuration list for PBXNativeTarget "NioKitTests" */; @@ -1112,7 +1235,7 @@ 39C931D123843289004449E1 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1250; + LastSwiftUpdateCheck = 1300; LastUpgradeCheck = 1250; ORGANIZATIONNAME = "Kilian Koeltzsch"; TargetAttributes = { @@ -1126,6 +1249,12 @@ 4BFEFD7B246F414D00CCF4A0 = { CreatedOnToolsVersion = 11.4.1; }; + 95CAC86626791295001D71DC = { + CreatedOnToolsVersion = 13.0; + }; + 95CAC87126791295001D71DC = { + CreatedOnToolsVersion = 13.0; + }; CADF663424614D2300F5063F = { CreatedOnToolsVersion = 11.4.1; LastSwiftMigration = 1140; @@ -1186,6 +1315,8 @@ E897AA1425F2706D00D11427 /* NioKit-macOS */, CADF663424614D2300F5063F /* NioKitTests */, 4BFEFD7B246F414D00CCF4A0 /* NioShareExtension */, + 95CAC86626791295001D71DC /* NioIntentsExtension */, + 95CAC87126791295001D71DC /* NioIntentsExtensionUI */, ); }; /* End PBXProject section */ @@ -1225,6 +1356,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95CAC86526791295001D71DC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 95CAC87026791295001D71DC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC87A26791295001D71DC /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CADF663324614D2300F5063F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1341,6 +1487,7 @@ CAC46D5923A272D10079C24F /* BorderedMessageView.swift in Sources */, 39B834C0243FC42000AE1EA0 /* TypingIndicatorView.swift in Sources */, 4BD867272460ADEF0014E3D6 /* NewConversationView.swift in Sources */, + 95CAC88E26791D11001D71DC /* AppDelegate.swift in Sources */, 3984654523B7ECBA006C173B /* MXURL.swift in Sources */, 392221AE243A0508004D8794 /* GroupedReactionsView.swift in Sources */, 3923898F2388707E00B2E1DF /* RoomListItemView.swift in Sources */, @@ -1406,6 +1553,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95CAC86326791295001D71DC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC89026792247001D71DC /* Intents.intentdefinition in Sources */, + 95CAC86C26791295001D71DC /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 95CAC86E26791295001D71DC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC87726791295001D71DC /* IntentViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CADF663124614D2300F5063F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1422,6 +1586,7 @@ E8B4725A25F26D5A00ACEFCB /* MX+Identifiable.swift in Sources */, E8B4725B25F26D5A00ACEFCB /* Reaction.swift in Sources */, 9525D2CF26763DCB004055B6 /* MXSession+Async.swift in Sources */, + 95CAC88B26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */, E8B4725525F26D5A00ACEFCB /* MXClient+Publisher.swift in Sources */, E897AA3425F2716F00D11427 /* UXKit.swift in Sources */, 9525D2D626763DD0004055B6 /* MXRoom+Async.swift in Sources */, @@ -1512,6 +1677,7 @@ E897AA2025F2707C00D11427 /* NIORoomSummary.swift in Sources */, E897AA2525F2707C00D11427 /* ReactionEvent.swift in Sources */, 9525D2D026763DCB004055B6 /* MXSession+Async.swift in Sources */, + 95CAC88C26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */, E897AA2225F2707C00D11427 /* AccountStore.swift in Sources */, E897AA3525F2716F00D11427 /* UXKit.swift in Sources */, 9525D2D526763DD0004055B6 /* MXRoom+Async.swift in Sources */, @@ -1544,6 +1710,16 @@ target = 4BFEFD7B246F414D00CCF4A0 /* NioShareExtension */; targetProxy = 4BFEFD84246F414D00CCF4A0 /* PBXContainerItemProxy */; }; + 95CAC87D26791295001D71DC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 95CAC87126791295001D71DC /* NioIntentsExtensionUI */; + targetProxy = 95CAC87C26791295001D71DC /* PBXContainerItemProxy */; + }; + 95CAC88126791295001D71DC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 95CAC86626791295001D71DC /* NioIntentsExtension */; + targetProxy = 95CAC88026791295001D71DC /* PBXContainerItemProxy */; + }; CADF663A24614D2300F5063F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 39C931D823843289004449E1 /* Nio */; @@ -1615,6 +1791,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 95CAC87826791295001D71DC /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 95CAC87926791295001D71DC /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1871,6 +2055,124 @@ }; name = Release; }; + 95CAC88426791295001D71DC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NioIntentsExtension/NioIntentsExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 33; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NioIntentsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtension; + INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtension; + INFOPLIST_KEY_CFBundleName = NioIntentsExtension; + INFOPLIST_KEY_CFBundleVersion = 1; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(INTENTS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 95CAC88526791295001D71DC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NioIntentsExtension/NioIntentsExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 33; + DEVELOPMENT_TEAM = HU85FER47E; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NioIntentsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtension; + INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtension; + INFOPLIST_KEY_CFBundleName = NioIntentsExtension; + INFOPLIST_KEY_CFBundleVersion = 1; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 95CAC88626791295001D71DC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 33; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NioIntentsExtensionUI/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtensionUI; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(INTENTSUI_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 95CAC88726791295001D71DC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 33; + DEVELOPMENT_TEAM = HU85FER47E; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NioIntentsExtensionUI/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtensionUI; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = chat.nio.iOS.NioIntentsExtensionUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; CADF664924614D2300F5063F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2063,6 +2365,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 95CAC88826791295001D71DC /* Build configuration list for PBXNativeTarget "NioIntentsExtensionUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 95CAC88626791295001D71DC /* Debug */, + 95CAC88726791295001D71DC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 95CAC88926791295001D71DC /* Build configuration list for PBXNativeTarget "NioIntentsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 95CAC88426791295001D71DC /* Debug */, + 95CAC88526791295001D71DC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; CADF664824614D2300F5063F /* Build configuration list for PBXNativeTarget "NioKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Nio.xcodeproj/xcshareddata/xcschemes/Nio-nolaunch.xcscheme b/Nio.xcodeproj/xcshareddata/xcschemes/Nio-nolaunch.xcscheme new file mode 100644 index 00000000..5df0f984 --- /dev/null +++ b/Nio.xcodeproj/xcshareddata/xcschemes/Nio-nolaunch.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nio.xcodeproj/xcshareddata/xcschemes/Nio.xcscheme b/Nio.xcodeproj/xcshareddata/xcschemes/Nio.xcscheme index ac2a8fbd..82b66907 100644 --- a/Nio.xcodeproj/xcshareddata/xcschemes/Nio.xcscheme +++ b/Nio.xcodeproj/xcshareddata/xcschemes/Nio.xcscheme @@ -75,6 +75,10 @@ argument = "-clear-stored-credentials" isEnabled = "NO"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtensionUI.xcscheme b/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtensionUI.xcscheme new file mode 100644 index 00000000..9608d853 --- /dev/null +++ b/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtensionUI.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nio.xcodeproj/xcshareddata/xcschemes/NioShareExtension.xcscheme b/Nio.xcodeproj/xcshareddata/xcschemes/NioShareExtension.xcscheme new file mode 100644 index 00000000..50f9e6f6 --- /dev/null +++ b/Nio.xcodeproj/xcshareddata/xcschemes/NioShareExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nio/AppDelegate.swift b/Nio/AppDelegate.swift new file mode 100644 index 00000000..fbdb1d0e --- /dev/null +++ b/Nio/AppDelegate.swift @@ -0,0 +1,81 @@ +// +// AppDelegate.swift +// Nio +// +// Created by Finn Behrens on 15.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Foundation +import NioIntentsExtension +import MatrixSDK +import Intents +import UserNotifications +import UIKit + +class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { + public static let shared = AppDelegate() + + @Published + var selectedRoom: MXRoom.MXRoomId? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + UNUserNotificationCenter.current().delegate = self + async { + do { + let state = try await UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) + print("state: \(state)") + } catch { + print("error requesting UNUserNotificationCenter: \(error.localizedDescription)") + } + } + + print("Your code here") + return true + } + + func application(_ application: UIApplication, + handlerFor intent: INIntent) -> Any? { + print("intent") + print(intent) + //return IntentHandler() + return nil + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + print("userActivity") + print(userActivity.activityType) + switch userActivity.activityType { + case "chat.nio.chat": + if let id = userActivity.userInfo?["id"] as? String { + print("restoring room \(id)") + Self.shared.selectedRoom = MXRoom.MXRoomId(id) + return true + } + default: + print("cannot handle type \(userActivity.activityType)") + } + return true + //return false + } +} + + + + // Conform to UNUserNotificationCenterDelegate +extension AppDelegate: UNUserNotificationCenterDelegate { + + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) + { + // TODO: does not seem to work, and also only do that for nio.chat.developer-settings.* + //UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notification.request.identifier]) + //UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notification.request.identifier]) + + + print("userNotificationCenter called") + completionHandler([.banner, .sound]) + } + +} diff --git a/Nio/Conversations/RecentRoomsContainerView.swift b/Nio/Conversations/RecentRoomsContainerView.swift index 0aeeb11f..76eced18 100644 --- a/Nio/Conversations/RecentRoomsContainerView.swift +++ b/Nio/Conversations/RecentRoomsContainerView.swift @@ -5,16 +5,27 @@ import Introspect import NioKit struct RecentRoomsContainerView: View { + + @ObservedObject var appDelegate = AppDelegate.shared + @EnvironmentObject var store: AccountStore @AppStorage("accentColor") var accentColor: Color = .purple @State private var selectedNavigationItem: SelectedNavigationItem? - @State private var selectedRoomId: ObjectIdentifier? + @State private var selectedRoomId: MXRoom.MXRoomId? private func autoselectFirstRoom() { - if selectedRoomId == nil { + /*if selectedRoomId == nil { selectedRoomId = store.rooms.first?.id - } + }*/ + } + + private func restoreChat() { + print("trying to restore selectedRoomId") + if let room = AppDelegate.shared.selectedRoom { + print("restoring seletedRoomId") + selectedRoomId = room + } } var body: some View { @@ -32,8 +43,12 @@ struct RecentRoomsContainerView: View { } .onAppear { self.store.startListeningForRoomEvents() + self.restoreChat() if #available(macOS 11, *) { autoselectFirstRoom() } } + .onChange(of: appDelegate.selectedRoom) { newRoom in + selectedRoomId = newRoom + } } } @@ -44,7 +59,7 @@ struct RoomsListSection: View { let rooms: [NIORoom] let onLeaveAlertTitle: String - @Binding var selectedRoomId: ObjectIdentifier? + @Binding var selectedRoomId: MXRoom.MXRoomId? @State private var showConfirm: Bool = false @State private var leaveId: Int? @@ -120,7 +135,7 @@ enum SelectedNavigationItem: Int, Identifiable { struct NavigationSheet: View { var selectedItem: SelectedNavigationItem - @Binding var selectedRoomId: ObjectIdentifier? + @Binding var selectedRoomId: MXRoom.MXRoomId? var body: some View { switch selectedItem { diff --git a/Nio/Conversations/RecentRoomsView.swift b/Nio/Conversations/RecentRoomsView.swift index f22485f3..f08fea7a 100644 --- a/Nio/Conversations/RecentRoomsView.swift +++ b/Nio/Conversations/RecentRoomsView.swift @@ -15,7 +15,7 @@ struct RecentRoomsView: View { @EnvironmentObject var store: AccountStore @Binding var selectedNavigationItem: SelectedNavigationItem? - @Binding var selectedRoomId: ObjectIdentifier? + @Binding var selectedRoomId: MXRoom.MXRoomId? let rooms: [NIORoom] diff --git a/Nio/Conversations/RoomContainerView.swift b/Nio/Conversations/RoomContainerView.swift index 22be8a73..554a4074 100644 --- a/Nio/Conversations/RoomContainerView.swift +++ b/Nio/Conversations/RoomContainerView.swift @@ -18,7 +18,9 @@ struct RoomContainerView: View { isDirect: room.isDirect, showAttachmentPicker: $showAttachmentPicker, onCommit: { message in - self.room.send(text: message) + asyncDetached { + await self.room.send(text: message) + } }, onReact: { eventId in self.eventToReactTo = eventId @@ -72,7 +74,9 @@ struct RoomContainerView: View { .background(EmptyView() .sheet(isPresented: $showImagePicker) { ImagePicker(sourceType: .photoLibrary) { image in - self.room.sendImage(image: image) + asyncDetached { + await self.room.sendImage(image: image) + } } } ) diff --git a/Nio/Conversations/RoomView.swift b/Nio/Conversations/RoomView.swift index 0686dba8..bc3a0cab 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -2,6 +2,10 @@ import Combine import MatrixSDK import SwiftUI +import Intents +import CoreSpotlight +import CoreServices + import NioKit struct RoomView: View { @@ -95,6 +99,25 @@ struct RoomView: View { primaryButton: .destructive(Text(verbatim: L10n.Room.Remove.action), action: { self.onRedact(eventId, nil) }), secondaryButton: .cancel()) } + .userActivity("chat.nio.chat") { userActivity in + userActivity.isEligibleForHandoff = true + userActivity.isEligibleForSearch = true + userActivity.isEligibleForPrediction = true + userActivity.title = room.displayName + userActivity.userInfo = ["id": room.id.rawValue as String] + + let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String) + + attributes.contentDescription = "Open chat with \(room.displayName)" + attributes.instantMessageAddresses = [ room.room.roomId ] + userActivity.contentAttributeSet = attributes + userActivity.webpageURL = URL(string: "https://matrix.to/#/\(room.room.roomId ?? "")") + + // TODO: implement with a viewDelegate to save the current text into the handsof + // userActivity.needsSave = true + + print("advertising: \(room.displayName) \(String(describing: userActivity.webpageURL))") + } } private func send() { diff --git a/Nio/Info.plist b/Nio/Info.plist index b22410e7..4ac9798e 100644 --- a/Nio/Info.plist +++ b/Nio/Info.plist @@ -2,6 +2,8 @@ + NSSiriUsageDescription + Send Messages via Siri AppGroup $(APPGROUP) CFBundleDevelopmentRegion @@ -62,8 +64,20 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + DevelopmentTeam + $(DEVELOPMENT_TEAM) + INIntentsSupported + + INSendMessageIntent + + LSApplicationCategoryType + public.app-category.social-networking LSRequiresIPhoneOS + NSUserActivityTypes + + chat.nio.chat + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -81,8 +95,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - DevelopmentTeam - $(DEVELOPMENT_TEAM) UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait @@ -90,5 +102,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UTImportedTypeDeclarations + + + diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 45c408df..abdca8b9 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -5,7 +5,7 @@ import NioKit struct NewConversationContainerView: View { @EnvironmentObject private var store: AccountStore - @Binding var createdRoomId: ObjectIdentifier? + @Binding var createdRoomId: MXRoom.MXRoomId? var body: some View { NewConversationView(store: store, createdRoomId: $createdRoomId) @@ -25,7 +25,7 @@ private struct NewConversationView: View { @State private var isPublic = false @State private var isWaiting = false - @Binding var createdRoomId: ObjectIdentifier? + @Binding var createdRoomId: MXRoom.MXRoomId? @State private var errorMessage: String? @MainActor @@ -103,7 +103,7 @@ private struct NewConversationView: View { ToolbarItem(placement: .confirmationAction) { Button(action: { async { - await createRoom + await createRoom() } }) { Text(verbatim: L10n.NewConversation.createRoom) @@ -176,7 +176,7 @@ private struct NewConversationView: View { await store?.session?.createRoom(parameters: parameters) { response in switch response { case .success(let room): - createdRoomId = room.id + createdRoomId = MXRoom.MXRoomId(room.roomId) presentationMode.wrappedValue.dismiss() case.failure(let error): errorMessage = error.localizedDescription diff --git a/Nio/Nio.entitlements b/Nio/Nio.entitlements index 21d5f383..94894ca9 100644 --- a/Nio/Nio.entitlements +++ b/Nio/Nio.entitlements @@ -2,6 +2,12 @@ + com.apple.developer.associated-domains + + https://matrix.to + + com.apple.developer.siri + com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/Nio/NioApp.swift b/Nio/NioApp.swift index 150379c7..9675da5d 100644 --- a/Nio/NioApp.swift +++ b/Nio/NioApp.swift @@ -1,9 +1,18 @@ import SwiftUI import NioKit +import MatrixSDK +import Intents @main struct NioApp: App { + #if os(macOS) + #else + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #endif + @StateObject private var accountStore = AccountStore() + + //@State private var selectedRoomId: ObjectIdentifier? @AppStorage("accentColor") private var accentColor: Color = .purple @@ -19,6 +28,25 @@ struct NioApp: App { RootView() .environmentObject(accountStore) .accentColor(accentColor) + .onContinueUserActivity("chat.nio.chat", perform: {activity in + print("handling activity: \(activity)") + if let id = activity.userInfo?["id"] as? String { + print("restored room: \(id)") + AppDelegate.shared.selectedRoom = MXRoom.MXRoomId(id) + } + /*if let id = activity.userInfo?["id"] as? String { + print("found string \(id)") + }*/ + }) + .onAppear { + async { + let _: INSiriAuthorizationStatus = await withCheckedContinuation {continuation in + INPreferences.requestSiriAuthorization({status in + continuation.resume(returning: status) + }) + } + } + } #endif } diff --git a/Nio/RootView.swift b/Nio/RootView.swift index de44f796..c9ad21f2 100644 --- a/Nio/RootView.swift +++ b/Nio/RootView.swift @@ -3,8 +3,9 @@ import SwiftUI import NioKit struct RootView: View { + @EnvironmentObject private var store: AccountStore - + private var homeserverURL: URL { // Can this ever be nil? And if so, what happens with the default fallback? assert(store.client != nil) @@ -34,7 +35,8 @@ struct RootView: View { Button(action: { self.store.loginState = .loggedOut }) { Text(verbatim: L10n.Login.failureBackToLogin) } - .padding() + .padding() + } } } diff --git a/Nio/Settings/SettingsContainerView.swift b/Nio/Settings/SettingsContainerView.swift index dfd13178..68b50524 100644 --- a/Nio/Settings/SettingsContainerView.swift +++ b/Nio/Settings/SettingsContainerView.swift @@ -24,11 +24,37 @@ struct SettingsContainerView: View { private struct SettingsView: View { @AppStorage("accentColor") private var accentColor: Color = .purple + @AppStorage("showDeveloperSettings") private var showDeveloperSettings = false + @StateObject private var appIconTitle = AppIconTitle() let logoutAction: () -> Void + + private let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String @Environment(\.presentationMode) private var presentationMode + /// Show a info banner for e.g. changing the developer setting + func showInfoBanner(_ text: String, identifier: String) { + print("trying to show banner") + asyncDetached { + let notification = UNMutableNotificationContent() + notification.body = text + notification.sound = UNNotificationSound.default + notification.userInfo = ["settings": identifier] + notification.title = "Settings changed" + notification.badge = await UIApplication.shared.applicationIconBadgeNumber as NSNumber + + let request = UNNotificationRequest(identifier: identifier, content: notification, trigger: nil) + + //request. + do { + try await UNUserNotificationCenter.current().add(request) + } catch { + print("failed to schedule notification: \(error.localizedDescription)") + } + } + } + var body: some View { NavigationView { Form { @@ -55,6 +81,28 @@ private struct SettingsView: View { Text(verbatim: L10n.Settings.logOut) } } + + Section("Version") { + Text(bundleVersion) + .onTapGesture { + showDeveloperSettings.toggle() + let text = showDeveloperSettings ? "Developer settings activated" : "Developer settings deactivated" + showInfoBanner(text, identifier: "chat.nio.developer-settings.show") + } + } + + if showDeveloperSettings { + Section("Developer") { + Button(action: { + async { + await AccountStore.deleteSkItems() + showInfoBanner("Sirikit Donations cleared", identifier: "chat.nio.developer-settings.sk-cleared") + } + }) { + Text("delete sk items") + } + } + } } .navigationBarTitle(L10n.Settings.title, displayMode: .inline) .toolbar { diff --git a/NioIntentsExtension/Info.plist b/NioIntentsExtension/Info.plist new file mode 100644 index 00000000..213ecc0d --- /dev/null +++ b/NioIntentsExtension/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + INSendMessageIntent + INSearchForMessagesIntent + INSetMessageAttributeIntent + + IntentsRestrictedWhileProtectedDataUnavailable + + INSendMessageIntent + INSearchForMessagesIntent + INSetMessageAttributeIntent + + IntentsSupported + + INSendMessageIntent + INSearchForMessagesIntent + INSetMessageAttributeIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/NioIntentsExtension/IntentHandler.swift b/NioIntentsExtension/IntentHandler.swift new file mode 100644 index 00000000..7aa906a3 --- /dev/null +++ b/NioIntentsExtension/IntentHandler.swift @@ -0,0 +1,125 @@ +// +// IntentHandler.swift +// NioIntentsExtension +// +// Created by Finn Behrens on 15.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Intents + +// As an example, this class is set up to handle Message intents. +// You will want to replace this or add other intents as appropriate. +// The intents you wish to handle must be declared in the extension's Info.plist. + +// You can test your example integration by saying things to Siri like: +// "Send a message using " +// " John saying hello" +// "Search for messages in " + +public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling { + + override public func handler(for intent: INIntent) -> Any { + // This is the default implementation. If you want different objects to handle different intents, + // you can override this and return the handler you want for that particular intent. + + return self + } + + // MARK: - INSendMessageIntentHandling + + // Implement resolution methods to provide additional information about your intent (optional). + public func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) { + if let recipients = intent.recipients { + + // If no recipients were provided we'll need to prompt for a value. + if recipients.count == 0 { + completion([INSendMessageRecipientResolutionResult.needsValue()]) + return + } + + var resolutionResults = [INSendMessageRecipientResolutionResult]() + for recipient in recipients { + let matchingContacts = [recipient] // Implement your contact matching logic here to create an array of matching contacts + switch matchingContacts.count { + case 2 ... Int.max: + // We need Siri's help to ask user to pick one from the matches. + resolutionResults += [INSendMessageRecipientResolutionResult.disambiguation(with: matchingContacts)] + + case 1: + // We have exactly one matching contact + resolutionResults += [INSendMessageRecipientResolutionResult.success(with: recipient)] + + case 0: + // We have no contacts matching the description provided + resolutionResults += [INSendMessageRecipientResolutionResult.unsupported()] + + default: + break + + } + } + completion(resolutionResults) + } else { + completion([INSendMessageRecipientResolutionResult.needsValue()]) + } + } + + public func resolveContent(for intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + if let text = intent.content, !text.isEmpty { + completion(INStringResolutionResult.success(with: text)) + } else { + completion(INStringResolutionResult.needsValue()) + } + } + + // Once resolution is completed, perform validation on the intent and provide confirmation (optional). + + public func confirm(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { + // Verify user is authenticated and your app is ready to send a message. + + let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) + let response = INSendMessageIntentResponse(code: .ready, userActivity: userActivity) + completion(response) + } + + // Handle the completed intent (required). + + public func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { + // Implement your application logic to send a message here. + + let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) + let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) + completion(response) + } + + // Implement handlers for each intent you wish to handle. As an example for messages, you may wish to also handle searchForMessages and setMessageAttributes. + + // MARK: - INSearchForMessagesIntentHandling + + public func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) { + // Implement your application logic to find a message that matches the information in the intent. + + let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) + let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity) + // Initialize with found message's attributes + response.messages = [INMessage( + identifier: "identifier", + content: "I am so excited about SiriKit!", + dateSent: Date(), + sender: INPerson(personHandle: INPersonHandle(value: "sarah@example.com", type: .emailAddress), nameComponents: nil, displayName: "Sarah", image: nil, contactIdentifier: nil, customIdentifier: nil), + recipients: [INPerson(personHandle: INPersonHandle(value: "+1-415-555-5555", type: .phoneNumber), nameComponents: nil, displayName: "John", image: nil, contactIdentifier: nil, customIdentifier: nil)] + )] + completion(response) + } + + // MARK: - INSetMessageAttributeIntentHandling + + public func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) { + // Implement your application logic to set the message attribute here. + + let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) + let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity) + completion(response) + } +} diff --git a/NioIntentsExtension/Intents.intentdefinition b/NioIntentsExtension/Intents.intentdefinition new file mode 100644 index 00000000..6b27dbb2 --- /dev/null +++ b/NioIntentsExtension/Intents.intentdefinition @@ -0,0 +1,299 @@ + + + + + INEnums + + INIntentDefinitionModelVersion + 1.2 + INIntentDefinitionNamespace + IpMFmo + INIntentDefinitionSystemVersion + 20G5023d + INIntentDefinitionToolsBuildVersion + 13A5154h + INIntentDefinitionToolsVersion + 13.0 + INIntents + + + INIntentCategory + system + INIntentClassName + INSendMessageIntent + INIntentClassPrefix + IN + INIntentCustomizable + + INIntentDescription + A request to send a message to the designated recipients. + INIntentDescriptionID + qsOBul + INIntentDomain + Messaging + INIntentInput + content + INIntentKeyParameter + recipients + INIntentLastParameterTag + 21 + INIntentName + SendMessage + INIntentParameterCombinations + + content,recipients + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Send “${content}” to ${recipients} + INIntentParameterCombinationTitleID + 2bXdOO + + content,speakableGroupName + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Send “${content}” to ${speakableGroupName} + INIntentParameterCombinationTitleID + Kr6YXh + + content,speakableGroupName,recipients + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Send “${content}” to ${speakableGroupName} + INIntentParameterCombinationTitleID + vvi9i0 + + recipients + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Send a message to ${recipients} + INIntentParameterCombinationTitleID + D7JjnM + + speakableGroupName + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Send a message to ${speakableGroupName} + INIntentParameterCombinationTitleID + quajA3 + + speakableGroupName,recipients + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Send a message to ${speakableGroupName} + INIntentParameterCombinationTitleID + FMCSms + + + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Group Name + INIntentParameterDisplayNameID + SendMessage-DisplayName-speakableGroupName + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + None + + INIntentParameterName + speakableGroupName + INIntentParameterSupportsResolution + + INIntentParameterTag + 3 + INIntentParameterType + SpeakableString + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Recipients + INIntentParameterDisplayNameID + SendMessage-DisplayName-recipients + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataType + Contact + + INIntentParameterName + recipients + INIntentParameterSupportsMultipleValues + + INIntentParameterSupportsResolution + + INIntentParameterTag + 20 + INIntentParameterType + Person + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Content + INIntentParameterDisplayNameID + SendMessage-DisplayName-content + INIntentParameterDisplayPriority + 3 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + None + INIntentParameterMetadataDefaultValueID + 1VOmYx + INIntentParameterMetadataKeyboardType + ASCIICapable + INIntentParameterMetadataPlaceholderID + KkByhy + + INIntentParameterName + content + INIntentParameterSupportsResolution + + INIntentParameterTag + 1 + INIntentParameterType + String + + + INIntentParameterDisplayName + Conversation Identifier + INIntentParameterDisplayNameID + tjmTwf + INIntentParameterDisplayPriority + 4 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + None + INIntentParameterMetadataDefaultValueID + n2NqOm + INIntentParameterMetadataKeyboardType + ASCIICapable + INIntentParameterMetadataPlaceholderID + Zuaw5t + + INIntentParameterName + conversationIdentifier + INIntentParameterTag + 9 + INIntentParameterType + String + + + INIntentParameterDisplayName + Service Name + INIntentParameterDisplayNameID + ByTbRO + INIntentParameterDisplayPriority + 5 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + None + INIntentParameterMetadataDefaultValueID + pYYeOW + INIntentParameterMetadataKeyboardType + ASCIICapable + INIntentParameterMetadataPlaceholderID + 4UssF1 + + INIntentParameterName + serviceName + INIntentParameterTag + 6 + INIntentParameterType + String + + + INIntentParameterDisplayName + Sender + INIntentParameterDisplayNameID + PCCAwS + INIntentParameterDisplayPriority + 6 + INIntentParameterMetadata + + INIntentParameterMetadataType + Contact + + INIntentParameterName + sender + INIntentParameterTag + 21 + INIntentParameterType + Person + + + INIntentParameterDisplayName + Outgoing Message Type + INIntentParameterDisplayNameID + r7jr7O + INIntentParameterDisplayPriority + 7 + INIntentParameterEnumType + OutgoingMessageType + INIntentParameterEnumTypeNamespace + System + INIntentParameterName + outgoingMessageType + INIntentParameterTag + 15 + INIntentParameterType + Integer + + + INIntentParameterDisplayName + Attachments + INIntentParameterDisplayNameID + v0qUig + INIntentParameterDisplayPriority + 8 + INIntentParameterName + attachments + INIntentParameterObjectType + MessageAttachment + INIntentParameterObjectTypeNamespace + System + INIntentParameterTag + 17 + INIntentParameterType + Object + + + INIntentTitle + Send Message + INIntentTitleID + mjLtxO + INIntentType + System + INIntentUserConfirmationRequired + + + + INTypes + + + diff --git a/NioIntentsExtension/NioIntentsExtension.entitlements b/NioIntentsExtension/NioIntentsExtension.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/NioIntentsExtension/NioIntentsExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/NioIntentsExtensionUI/Base.lproj/MainInterface.storyboard b/NioIntentsExtensionUI/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..e6c64fcf --- /dev/null +++ b/NioIntentsExtensionUI/Base.lproj/MainInterface.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NioIntentsExtensionUI/Info.plist b/NioIntentsExtensionUI/Info.plist new file mode 100644 index 00000000..62c8c8a2 --- /dev/null +++ b/NioIntentsExtensionUI/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.intents-ui-service + + + diff --git a/NioIntentsExtensionUI/IntentViewController.swift b/NioIntentsExtensionUI/IntentViewController.swift new file mode 100644 index 00000000..c6b772f2 --- /dev/null +++ b/NioIntentsExtensionUI/IntentViewController.swift @@ -0,0 +1,37 @@ +// +// IntentViewController.swift +// NioIntentsExtensionUI +// +// Created by Finn Behrens on 15.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import IntentsUI + +// As an example, this extension's Info.plist has been configured to handle interactions for INSendMessageIntent. +// You will want to replace this or add other intents as appropriate. +// The intents whose interactions you wish to handle must be declared in the extension's Info.plist. + +// You can test this example integration by saying things to Siri like: +// "Send a message using " + +class IntentViewController: UIViewController, INUIHostedViewControlling { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + // MARK: - INUIHostedViewControlling + + // Prepare your view controller for the interaction to handle. + func configureView(for parameters: Set, of interaction: INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: INUIHostedViewContext, completion: @escaping (Bool, Set, CGSize) -> Void) { + // Do configuration here, including preparing views and calculating a desired size for presentation. + completion(true, parameters, self.desiredSize) + } + + var desiredSize: CGSize { + return self.extensionContext!.hostedViewMaximumAllowedSize + } + +} diff --git a/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements b/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/NioKit/Extensions/MX+Identifiable.swift b/NioKit/Extensions/MX+Identifiable.swift index fa7a97ba..05e88fff 100644 --- a/NioKit/Extensions/MX+Identifiable.swift +++ b/NioKit/Extensions/MX+Identifiable.swift @@ -1,6 +1,65 @@ import MatrixSDK import SwiftUI +public protocol MXStringId: Hashable, Codable, ExpressibleByStringLiteral, + CustomStringConvertible, RawRepresentable { + +var id : String { get } + +init(_ id: String) +} + +extension MXStringId { + @inlinable + public init(rawValue id: String) { self.init(id) } + + @inlinable + public var rawValue : String { return id } +} + +public extension MXStringId { + + @inlinable + var description: String { return "<\(type(of: self)): \(id)>" } +} + +public extension MXStringId { // Literals + + init(stringLiteral value: String) { self.init(value) } +} + +public extension MXStringId { + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.init(try container.decode(String.self)) + } + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(id) + } +} + +public extension MXStringId { + init(_ id: R) where R: RawRepresentable, R.RawValue == String { + self.init(id.rawValue) + } +} + extension MXPublicRoom: Identifiable {} -extension MXRoom: Identifiable {} +extension MXRoom: Identifiable { + public struct MXRoomId: MXStringId, Hashable { + public var id: String + + public init(_ id: String) { + self.id = id + } + } + + public var id: MXRoomId { + get { + return MXRoomId(self.roomId) + } + } +} extension MXEvent: Identifiable {} diff --git a/NioKit/Extensions/MXRoom+Async.swift b/NioKit/Extensions/MXRoom+Async.swift index 1f1732e5..60ab0af1 100644 --- a/NioKit/Extensions/MXRoom+Async.swift +++ b/NioKit/Extensions/MXRoom+Async.swift @@ -31,4 +31,36 @@ extension MXRoom { } } } + + @discardableResult + func sendTextMessage(_ text: String, formattedText: String? = nil, localEcho: inout MXEvent?) async throws -> String? { + return try await withCheckedThrowingContinuation {continuation in + self.sendTextMessage(text, formattedText: formattedText, localEcho: &localEcho, completion: {resp in + switch resp { + case let .success(v): + continuation.resume(returning: v) + case let .failure(e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } + + @discardableResult + func sendImage(data: Data, size: CGSize, mimeType: String, thumbnail: MXImage? = nil, localEcho: inout MXEvent?) async throws -> String? { + return try await withCheckedThrowingContinuation {continuation in + self.sendImage(data: data, size: size, mimeType: mimeType, thumbnail: thumbnail, localEcho: &localEcho, completion: {resp in + switch resp { + case let .success(v): + continuation.resume(returning: v) + case let .failure(e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } } diff --git a/NioKit/Extensions/MXRoomMember+INPerson.swift b/NioKit/Extensions/MXRoomMember+INPerson.swift new file mode 100644 index 00000000..6cd27c6e --- /dev/null +++ b/NioKit/Extensions/MXRoomMember+INPerson.swift @@ -0,0 +1,36 @@ +// +// MXRoomMember+INPerson.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +import Intents +import MatrixSDK + +extension MXRoomMember { + var inPerson: INPerson { + let inImage = self.avatarUrl.flatMap { avatar in + URL(string: avatar) + }.flatMap { url in + INImage(url: url) + } + return INPerson( + personHandle: INPersonHandle(value: self.userId, type: .unknown), + nameComponents: nil, // TODO + displayName: self.displayname, + image: inImage, + contactIdentifier: nil, + customIdentifier: self.userId, + isMe: false, + suggestionType: .instantMessageAddress + ) + } +} + +extension MXRoomMembers { + var inPerson: [INPerson] { + self.members.map { $0.inPerson } + } +} diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index 43d0734e..d39aa72e 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -4,6 +4,9 @@ import Combine import MatrixSDK import os +import Intents +import CoreSpotlight +import CoreServices public struct RoomItem: Codable, Hashable { public static func == (lhs: RoomItem, rhs: RoomItem) -> Bool { @@ -112,17 +115,22 @@ public class NIORoom: ObservableObject { // MARK: Sending Events - public func send(text: String) { + public func send(text: String) async { guard !text.isEmpty else { return } objectWillChange.send() // room.outgoingMessages() will change var localEcho: MXEvent? = nil - // TODO: async - room.sendTextMessage(text, localEcho: &localEcho) { _ in - self.objectWillChange.send() // localEcho.sentState has(!) changed + do { + try await room.sendTextMessage(text, localEcho: &localEcho) + } catch { + Self.logger.warning("could not send text message to \(self.displayName): \(error.localizedDescription)") } + // localEcho.sentState has(!) changed + self.objectWillChange.send() + + await self.donateOutgoingIntent(text) } - + public func react(toEventId eventId: String, emoji: String) { // swiftlint:disable:next force_try let content = try! ReactionEvent(eventId: eventId, key: emoji).encodeContent() @@ -151,21 +159,26 @@ public class NIORoom: ObservableObject { room.redactEvent(eventId, reason: reason) { _ in } } - public func sendImage(image: UXImage) { + public func sendImage(image: UXImage) async { guard let imageData = image.jpeg(.lowest) else { return } var localEcho: MXEvent? = nil objectWillChange.send() // room.outgoingMessages() will change - // TODO: async - room.sendImage( - data: imageData, - size: image.size, - mimeType: "image/jpeg", - thumbnail: image, - localEcho: &localEcho - ) { _ in - self.objectWillChange.send() // localEcho.sentState has(!) changed + do { + try await room.sendImage( + data: imageData, + size: image.size, + mimeType: "image/jpeg", + thumbnail: image, + localEcho: &localEcho) + } catch { + Self.logger.warning("could not send image to \(self.displayName): \(error.localizedDescription)") } + // localEcho.sentState has(!) changed + self.objectWillChange.send() + + + await self.donateOutgoingIntent() } public func markAllAsRead() { @@ -177,6 +190,56 @@ public class NIORoom: ObservableObject { room.removeOutgoingMessage(event.eventId) } + // intent + private func donateOutgoingIntent(_ text: String? = nil) async { + do { + let recipients = try await self.room.members()?.inPerson + + let senderPersonHandle = INPersonHandle(value: room.mxSession.credentials.userId, type: .unknown) + let sender = INPerson( + personHandle: senderPersonHandle, + nameComponents: nil, + displayName: nil, + image: nil, + contactIdentifier: nil, + customIdentifier: room.mxSession.credentials.userId, + isMe: true, + suggestionType: .instantMessageAddress) + + let messageIntent = INSendMessageIntent( + recipients: recipients, + outgoingMessageType: .outgoingMessageText, + content: text, + speakableGroupName: INSpeakableString(spokenPhrase: room.summary.displayname), + conversationIdentifier: room.roomId, + serviceName: "matrix", + sender: sender, + attachments: nil) + + let userActivity = NSUserActivity(activityType: "chat.nio.chat") + userActivity.isEligibleForHandoff = true + userActivity.isEligibleForSearch = true + userActivity.isEligibleForPrediction = true + userActivity.title = self.displayName + userActivity.userInfo = ["id": self.id.rawValue as String] + + let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String) + + attributes.contentDescription = "Open chat with \(self.displayName)" + attributes.instantMessageAddresses = [ self.room.roomId ] + userActivity.contentAttributeSet = attributes + userActivity.webpageURL = URL(string: "https://matrix.to/#/\(self.room.roomId ?? "")") + let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) + + let intent = INInteraction(intent: messageIntent, response: response) + intent.direction = .outgoing + //intent.intentHandlingStatus = .success + try await intent.donate() + } catch { + Self.logger.warning("Could not donate intent: \(error.localizedDescription)") + } + } + //private var lastPaginatedEvent: MXEvent? private var timeline: MXEventTimeline? @@ -186,7 +249,8 @@ public class NIORoom: ObservableObject { }*/ if timeline == nil { - Self.logger.debug("creating timeline for room '\(self.displayName)' with event '\(event.eventId)'") + return await createPagination() + /*Self.logger.debug("creating timeline for room '\(self.displayName)' with event '\(event.eventId)'") //lastPaginatedEvent = event timeline = room.timeline(onEvent: event.eventId) let _ = timeline?.listenToEvents { @@ -196,7 +260,7 @@ public class NIORoom: ObservableObject { self.add(event: event, direction: direction, roomState: roomState) } } - timeline?.resetPagination() + timeline?.resetPagination()*/ } if timeline?.canPaginate(direction) ?? false { @@ -231,7 +295,7 @@ public class NIORoom: ObservableObject { } extension NIORoom: Identifiable { - public nonisolated var id: ObjectIdentifier { + public nonisolated var id: MXRoom.MXRoomId { room.id } } diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 49e6b905..173c15cf 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -1,7 +1,9 @@ import Combine import Foundation import KeychainAccess +import Intents import MatrixSDK +import os public enum LoginState { case loggedOut @@ -12,6 +14,8 @@ public enum LoginState { @MainActor public class AccountStore: ObservableObject { + static let logger = Logger(subsystem: "chat.nio.chat", category: "AccountStore") + public var client: MXRestClient? public var session: MXSession? @@ -30,6 +34,12 @@ public class AccountStore: ObservableObject { .from(keychain)? .clear(from: keychain) } + if CommandLine.arguments.contains("-clear-stored-sk-search-iterms") { + print("🗑 cleared stored sk search items from Siri") + async { + await Self.deleteSkItems() + } + } Configuration.setupMatrixSDKSettings() guard let credentials = MXCredentials.from(keychain) else { @@ -87,6 +97,17 @@ public class AccountStore: ObservableObject { // Close the session even if the logout request failed loginState = .loggedOut } + await NSUserActivity.deleteAllSavedUserActivities() + } + + public static func deleteSkItems() async { + await NSUserActivity.deleteAllSavedUserActivities() + do { + try await INInteraction.deleteAll() + Self.logger.debug("deleted ININteractions") + } catch { + Self.logger.warning("failed to delete INInteractions: \(error.localizedDescription)") + } } @available(*, deprecated, message: "Prefer async alternative instead") @@ -174,6 +195,15 @@ public class AccountStore: ObservableObject { self.objectWillChange.send() } } + + // MARK: - Push Notifications + /* + func requestNotificationToken() { + + }*/ + /*func setPusher() { + self.session?.matrixRestClient.setPusher(pushKey: <#T##String#>, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) + }*/ } enum AccountStoreError: Error { diff --git a/NioShareExtension/ShareViewController.swift b/NioShareExtension/ShareViewController.swift index c6793d8f..e7eb603e 100644 --- a/NioShareExtension/ShareViewController.swift +++ b/NioShareExtension/ShareViewController.swift @@ -34,8 +34,10 @@ class ShareNavigationController: UIViewController { let url = results["url"] as? String else { return } - for room in rooms where room.summary.roomId == roomID { - room.send(text: url) + async { + for room in rooms where room.summary.roomId == roomID { + await room.send(text: url) + } } self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } From a415d7e24e0d2a0e18a1214a641ade912d5a2344 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Sat, 19 Jun 2021 22:55:29 +0200 Subject: [PATCH 05/11] WIP: notifications --- Configs/GlobalConfig.xcconfig | 19 +- Nio.xcodeproj/project.pbxproj | 191 +++++++++++++++++- Nio/AppDelegate.swift | 124 +++++++++++- Nio/Info.plist | 14 +- Nio/Nio.entitlements | 4 + Nio/NioApp.swift | 2 +- Nio/Settings/SettingsContainerView.swift | 29 ++- NioIntentsExtension/Info.plist | 11 +- NioIntentsExtensionUI/Info.plist | 6 +- .../IntentViewController.swift | 2 + NioKit/Extensions/MX+Identifiable.swift | 17 +- NioKit/Extensions/MXRestClient+Async.swift | 39 ++++ NioKit/Extensions/MXRoom+Async.swift | 16 ++ NioKit/Extensions/MXRoomMember+INPerson.swift | 56 ++++- NioKit/Extensions/MXSession+Async.swift | 8 + NioKit/Models/Custom Events/ReplyEvent.swift | 85 ++++++++ NioKit/Models/NIORoom.swift | 133 +++++++++++- NioKit/Session/AccountStore.swift | 78 ++++++- NioNSE/Info.plist | 19 ++ NioNSE/NioNSE.entitlements | 15 ++ NioNSE/NioNSEDebug.entitlements | 19 ++ NioNSE/NotificationService.swift | 136 +++++++++++++ 22 files changed, 962 insertions(+), 61 deletions(-) create mode 100644 NioKit/Models/Custom Events/ReplyEvent.swift create mode 100644 NioNSE/Info.plist create mode 100644 NioNSE/NioNSE.entitlements create mode 100644 NioNSE/NioNSEDebug.entitlements create mode 100644 NioNSE/NotificationService.swift diff --git a/Configs/GlobalConfig.xcconfig b/Configs/GlobalConfig.xcconfig index e6e55fb7..67a71eed 100644 --- a/Configs/GlobalConfig.xcconfig +++ b/Configs/GlobalConfig.xcconfig @@ -8,15 +8,16 @@ // NIO_NAMESPACE = com.example.nio // DEVELOPMENT_TEAM = Z123456789 -NIO_NAMESPACE = com.example.nio -DEVELOPMENT_TEAM = Z123456789 +NIO_NAMESPACE = com.example.nio +DEVELOPMENT_TEAM = Z123456789 -APPGROUP = $(NIO_NAMESPACE) -PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS -MAC_PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).mio -SHARE_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioShareExtension -INTENTS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioIntents -INTENTSUI_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioIntentsUI -CODE_SIGN_STYLE = Manual +APPGROUP = $(NIO_NAMESPACE) +PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS +MAC_PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).mio +SHARE_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioShareExtension +INTENTS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioIntents +INTENTSUI_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioIntentsUI +NOTIFICATIONS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).NioNotificationExtension +CODE_SIGN_STYLE = Manual #include? "LocalConfig.xcconfig" diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 6a13a126..5ed70b62 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -61,6 +61,13 @@ 4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BFEFD8C246F458000CCF4A0 /* GetURL.js in Resources */ = {isa = PBXBuildFile; fileRef = 4BFEFD8B246F458000CCF4A0 /* GetURL.js */; }; 4BFEFD92246F686000CCF4A0 /* ShareContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */; }; + 95036C96267DF7A300E1EDC0 /* libNioKit-iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8302D4C25F26CA500E962E9 /* libNioKit-iOS.a */; }; + 95036C9A267E1B8300E1EDC0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88F26792247001D71DC /* Intents.intentdefinition */; }; + 95036C9B267E1B8500E1EDC0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88F26792247001D71DC /* Intents.intentdefinition */; }; + 95036C9C267E1BAA00E1EDC0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88F26792247001D71DC /* Intents.intentdefinition */; }; + 95036C9D267E33DE00E1EDC0 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88F26792247001D71DC /* Intents.intentdefinition */; }; + 95036C9F267E83D000E1EDC0 /* ReplyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95036C9E267E83D000E1EDC0 /* ReplyEvent.swift */; }; + 95036CA0267E83D000E1EDC0 /* ReplyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95036C9E267E83D000E1EDC0 /* ReplyEvent.swift */; }; 9525D2CF26763DCB004055B6 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CD26763D74004055B6 /* MXSession+Async.swift */; }; 9525D2D026763DCB004055B6 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CD26763D74004055B6 /* MXSession+Async.swift */; }; 9525D2D126763DD0004055B6 /* MXRestClient+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */; }; @@ -79,6 +86,8 @@ 9525D2EC267659A2004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */; }; 9525D2ED267659BD004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E62676594F004055B6 /* SettingsContainerView.swift */; }; 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; + 95A11CD2267CA8810094AC5F /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A11CD1267CA8810094AC5F /* NotificationService.swift */; }; + 95A11CD6267CA8810094AC5F /* NioNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 95A11CCF267CA8810094AC5F /* NioNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; 95CAC7B92678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; 95CAC86926791295001D71DC /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95CAC86826791295001D71DC /* Intents.framework */; }; @@ -230,6 +239,20 @@ remoteGlobalIDString = 4BFEFD7B246F414D00CCF4A0; remoteInfo = NioShareExtension; }; + 95036C97267DF7A300E1EDC0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39C931D123843289004449E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8302D4B25F26CA500E962E9; + remoteInfo = "NioKit-iOS"; + }; + 95A11CD4267CA8810094AC5F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39C931D123843289004449E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 95A11CCE267CA8810094AC5F; + remoteInfo = NioNSE; + }; 95CAC87C26791295001D71DC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 39C931D123843289004449E1 /* Project object */; @@ -281,6 +304,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 95A11CD6267CA8810094AC5F /* NioNSE.appex in Embed App Extensions */, 4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */, 95CAC87E26791295001D71DC /* NioIntentsExtensionUI.appex in Embed App Extensions */, 95CAC88226791295001D71DC /* NioIntentsExtension.appex in Embed App Extensions */, @@ -400,6 +424,8 @@ 4BFEFD8F246F5EE000CCF4A0 /* NioShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioShareExtension.entitlements; sourceTree = ""; }; 4BFEFD90246F5EF500CCF4A0 /* Nio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nio.entitlements; sourceTree = ""; }; 4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContentView.swift; sourceTree = ""; }; + 95036C99267DF86900E1EDC0 /* NioNSEDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioNSEDebug.entitlements; sourceTree = ""; }; + 95036C9E267E83D000E1EDC0 /* ReplyEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyEvent.swift; sourceTree = ""; }; 9525D2C726763CD4004055B6 /* MXAutoDiscovery+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXAutoDiscovery+Async.swift"; sourceTree = ""; }; 9525D2C826763CD4004055B6 /* MXRoom+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRoom+Async.swift"; sourceTree = ""; }; 9525D2CB26763D06004055B6 /* MXRestClient+Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRestClient+Async.swift"; sourceTree = ""; }; @@ -412,6 +438,10 @@ 9525D2E326765850004055B6 /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; 9525D2E62676594F004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; + 95A11CCF267CA8810094AC5F /* NioNSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NioNSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 95A11CD1267CA8810094AC5F /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 95A11CD3267CA8810094AC5F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 95A11CD7267CA8810094AC5F /* NioNSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioNSE.entitlements; sourceTree = ""; }; 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXEventTimeLine+Async.swift"; sourceTree = ""; }; 95CAC86726791295001D71DC /* NioIntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NioIntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 95CAC86826791295001D71DC /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; @@ -427,6 +457,8 @@ 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MXRoomMember+INPerson.swift"; sourceTree = ""; }; 95CAC88D26791D11001D71DC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 95CAC88F26792247001D71DC /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; }; + 95DE061F267B4D5800832844 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 95DE0621267B4D5800832844 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NioApp.swift; sourceTree = ""; }; A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageTextViewWrapper.swift; sourceTree = ""; }; CAAF5BF72478696F006FDC33 /* UITextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextViewWrapper.swift; sourceTree = ""; }; @@ -495,6 +527,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95A11CCC267CA8810094AC5F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 95036C96267DF7A300E1EDC0 /* libNioKit-iOS.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 95CAC86426791295001D71DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -635,6 +675,7 @@ 39222198243689D6004D8794 /* ReactionEvent.swift */, 4B058B5524573A570059BC75 /* EditEvent.swift */, 3922219C24368B25004D8794 /* CustomEvent.swift */, + 95036C9E267E83D000E1EDC0 /* ReplyEvent.swift */, ); path = "Custom Events"; sourceTree = ""; @@ -710,6 +751,7 @@ E843F2DB25F2748200B0F33B /* Mio */, 95CAC86A26791295001D71DC /* NioIntentsExtension */, 95CAC87526791295001D71DC /* NioIntentsExtensionUI */, + 95A11CD0267CA8810094AC5F /* NioNSE */, 39C931DA2384328A004449E1 /* Products */, A58352AA25A667B300533363 /* Frameworks */, ); @@ -728,6 +770,7 @@ E843F2DA25F2748200B0F33B /* Mio.app */, 95CAC86726791295001D71DC /* NioIntentsExtension.appex */, 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */, + 95A11CCF267CA8810094AC5F /* NioNSE.appex */, ); name = Products; sourceTree = ""; @@ -888,6 +931,17 @@ path = Settings; sourceTree = ""; }; + 95A11CD0267CA8810094AC5F /* NioNSE */ = { + isa = PBXGroup; + children = ( + 95036C99267DF86900E1EDC0 /* NioNSEDebug.entitlements */, + 95A11CD7267CA8810094AC5F /* NioNSE.entitlements */, + 95A11CD1267CA8810094AC5F /* NotificationService.swift */, + 95A11CD3267CA8810094AC5F /* Info.plist */, + ); + path = NioNSE; + sourceTree = ""; + }; 95CAC86A26791295001D71DC /* NioIntentsExtension */ = { isa = PBXGroup; children = ( @@ -915,6 +969,8 @@ children = ( 95CAC86826791295001D71DC /* Intents.framework */, 95CAC87326791295001D71DC /* IntentsUI.framework */, + 95DE061F267B4D5800832844 /* UserNotifications.framework */, + 95DE0621267B4D5800832844 /* UserNotificationsUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -1076,6 +1132,7 @@ 4BFEFD85246F414D00CCF4A0 /* PBXTargetDependency */, 95CAC87D26791295001D71DC /* PBXTargetDependency */, 95CAC88126791295001D71DC /* PBXTargetDependency */, + 95A11CD5267CA8810094AC5F /* PBXTargetDependency */, ); name = Nio; packageProductDependencies = ( @@ -1107,6 +1164,24 @@ productReference = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 95A11CCE267CA8810094AC5F /* NioNSE */ = { + isa = PBXNativeTarget; + buildConfigurationList = 95A11CD8267CA8810094AC5F /* Build configuration list for PBXNativeTarget "NioNSE" */; + buildPhases = ( + 95A11CCB267CA8810094AC5F /* Sources */, + 95A11CCC267CA8810094AC5F /* Frameworks */, + 95A11CCD267CA8810094AC5F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 95036C98267DF7A300E1EDC0 /* PBXTargetDependency */, + ); + name = NioNSE; + productName = NioNSE; + productReference = 95A11CCF267CA8810094AC5F /* NioNSE.appex */; + productType = "com.apple.product-type.app-extension"; + }; 95CAC86626791295001D71DC /* NioIntentsExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 95CAC88926791295001D71DC /* Build configuration list for PBXNativeTarget "NioIntentsExtension" */; @@ -1249,6 +1324,9 @@ 4BFEFD7B246F414D00CCF4A0 = { CreatedOnToolsVersion = 11.4.1; }; + 95A11CCE267CA8810094AC5F = { + CreatedOnToolsVersion = 13.0; + }; 95CAC86626791295001D71DC = { CreatedOnToolsVersion = 13.0; }; @@ -1317,6 +1395,7 @@ 4BFEFD7B246F414D00CCF4A0 /* NioShareExtension */, 95CAC86626791295001D71DC /* NioIntentsExtension */, 95CAC87126791295001D71DC /* NioIntentsExtensionUI */, + 95A11CCE267CA8810094AC5F /* NioNSE */, ); }; /* End PBXProject section */ @@ -1356,6 +1435,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95A11CCD267CA8810094AC5F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 95CAC86526791295001D71DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1503,6 +1589,7 @@ 392221B2243D0CCC004D8794 /* RoomTopicEventView.swift in Sources */, 3902B8A52395A77800698B87 /* LoadingView.swift in Sources */, CADF662424614A3300F5063F /* ReactionGroupView.swift in Sources */, + 95036C9A267E1B8300E1EDC0 /* Intents.intentdefinition in Sources */, 392221B6243F88FD004D8794 /* RoomNameEventView.swift in Sources */, 4B0A2E47245E2EF800A79443 /* MultilineTextField.swift in Sources */, A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */, @@ -1553,6 +1640,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95A11CCB267CA8810094AC5F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 95036C9B267E1B8500E1EDC0 /* Intents.intentdefinition in Sources */, + 95A11CD2267CA8810094AC5F /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 95CAC86326791295001D71DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1566,6 +1662,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 95036C9C267E1BAA00E1EDC0 /* Intents.intentdefinition in Sources */, 95CAC87726791295001D71DC /* IntentViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1584,6 +1681,7 @@ files = ( 9525D2D226763DD0004055B6 /* MXRestClient+Async.swift in Sources */, E8B4725A25F26D5A00ACEFCB /* MX+Identifiable.swift in Sources */, + 95036C9F267E83D000E1EDC0 /* ReplyEvent.swift in Sources */, E8B4725B25F26D5A00ACEFCB /* Reaction.swift in Sources */, 9525D2CF26763DCB004055B6 /* MXSession+Async.swift in Sources */, 95CAC88B26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */, @@ -1596,6 +1694,7 @@ E8B4726125F26D5A00ACEFCB /* AccountStore.swift in Sources */, E8B4725825F26D5A00ACEFCB /* CustomEvent.swift in Sources */, E8B4725F25F26D5A00ACEFCB /* Configuration.swift in Sources */, + 95036C9D267E33DE00E1EDC0 /* Intents.intentdefinition in Sources */, 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */, E8B4725D25F26D5A00ACEFCB /* EventCollection.swift in Sources */, 9525D2D426763DD0004055B6 /* MXAutoDiscovery+Async.swift in Sources */, @@ -1694,6 +1793,7 @@ E897AA2125F2707C00D11427 /* EventCollection.swift in Sources */, E897AA2625F2707C00D11427 /* UserDefaults.swift in Sources */, E897AA1A25F2707C00D11427 /* NIORoom.swift in Sources */, + 95036CA0267E83D000E1EDC0 /* ReplyEvent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1710,6 +1810,16 @@ target = 4BFEFD7B246F414D00CCF4A0 /* NioShareExtension */; targetProxy = 4BFEFD84246F414D00CCF4A0 /* PBXContainerItemProxy */; }; + 95036C98267DF7A300E1EDC0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E8302D4B25F26CA500E962E9 /* NioKit-iOS */; + targetProxy = 95036C97267DF7A300E1EDC0 /* PBXContainerItemProxy */; + }; + 95A11CD5267CA8810094AC5F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 95A11CCE267CA8810094AC5F /* NioNSE */; + targetProxy = 95A11CD4267CA8810094AC5F /* PBXContainerItemProxy */; + }; 95CAC87D26791295001D71DC /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 95CAC87126791295001D71DC /* NioIntentsExtensionUI */; @@ -2055,6 +2165,62 @@ }; name = Release; }; + 95A11CD9267CA8810094AC5F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NioNSE/NioNSEDebug.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 33; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NioNSE/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NioNSE; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(NOTIFICATIONS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 95A11CDA267CA8810094AC5F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NioNSE/NioNSE.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 33; + DEVELOPMENT_TEAM = HU85FER47E; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NioNSE/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NioNSE; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = chat.nio.iOS.NioNSE; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 95CAC88426791295001D71DC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2067,7 +2233,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtension; INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtension; INFOPLIST_KEY_CFBundleName = NioIntentsExtension; - INFOPLIST_KEY_CFBundleVersion = 1; + INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2076,7 +2242,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(INTENTS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_BUNDLE_IDENTIFIER = dev.kloenk.nio.iOS.NioIntents; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -2098,7 +2264,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtension; INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtension; INFOPLIST_KEY_CFBundleName = NioIntentsExtension; - INFOPLIST_KEY_CFBundleVersion = 1; + INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2107,7 +2273,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_BUNDLE_IDENTIFIER = dev.kloenk.nio.iOS.NioIntents; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -2127,6 +2293,9 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NioIntentsExtensionUI/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleName = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2155,6 +2324,9 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NioIntentsExtensionUI/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleName = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2163,7 +2335,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = chat.nio.iOS.NioIntentsExtensionUI; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -2365,6 +2537,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 95A11CD8267CA8810094AC5F /* Build configuration list for PBXNativeTarget "NioNSE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 95A11CD9267CA8810094AC5F /* Debug */, + 95A11CDA267CA8810094AC5F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 95CAC88826791295001D71DC /* Build configuration list for PBXNativeTarget "NioIntentsExtensionUI" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Nio/AppDelegate.swift b/Nio/AppDelegate.swift index fbdb1d0e..5538e75d 100644 --- a/Nio/AppDelegate.swift +++ b/Nio/AppDelegate.swift @@ -12,6 +12,7 @@ import MatrixSDK import Intents import UserNotifications import UIKit +import NioKit class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { public static let shared = AppDelegate() @@ -19,12 +20,27 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { @Published var selectedRoom: MXRoom.MXRoomId? + var isPushAllowed: Bool = false + + func application(_ application: UIApplication, + willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + print("willFinishLaunschingWithOptions") + + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.delegate = self + return true + } + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - UNUserNotificationCenter.current().delegate = self + let notificationCenter = UNUserNotificationCenter.current() + + self.createMessageActions(notificationCenter: notificationCenter) + async { do { - let state = try await UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) - print("state: \(state)") + let state = try await notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) + Self.shared.isPushAllowed = state + application.registerForRemoteNotifications() } catch { print("error requesting UNUserNotificationCenter: \(error.localizedDescription)") } @@ -42,6 +58,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { return nil } + // TODO: remove?? (should have been replaced by swiftui) func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { print("userActivity") print(userActivity.activityType) @@ -58,6 +75,23 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { return true //return false } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + let tokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)}) + print("this will return '32 bytes' in iOS 13+ rather than the token \(tokenString)") + async { + do { + try await AccountStore.shared.setPusher(key: deviceToken) + print("set pusher") + } catch { + print("could not set pusher: \(error.localizedDescription)") + } + } + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + print("error registering token: \(error.localizedDescription)") + } } @@ -65,17 +99,95 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { // Conform to UNUserNotificationCenterDelegate extension AppDelegate: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, + /*func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + + print("got notification: \(notification)") // TODO: does not seem to work, and also only do that for nio.chat.developer-settings.* //UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notification.request.identifier]) //UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notification.request.identifier]) - - print("userNotificationCenter called") completionHandler([.banner, .sound]) + }*/ + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + print("got notification \(notification)") + + return [.banner, .sound] } + @MainActor + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + let store = AccountStore.shared + print("did receive: \(response.actionIdentifier)") + + while store.loginState.isAuthenticating { + usleep(2000) + print("logging in") + } + + let roomId = MXRoom.MXRoomId(response.notification.request.content.userInfo["room_id"] as? String ?? "") + let room = store.findRoom(id: roomId) + let eventId = MXEvent.MXEventId(response.notification.request.content.userInfo["event_id"] as? String ?? "") + + let actionIdentifier = response.actionIdentifier + if actionIdentifier.starts(with: "chat.nio.reaction.emoji") { + guard let room = room else { + return + } + let emoji: String + switch actionIdentifier { + case "chat.nio.reaction.emoji.like": + emoji = "👍" + case "chat.nio.reaction.emoji.dislike": + emoji = "👎" + default: + print("invalid emoji ") + return + } + print("reacting with \(emoji)") + await room.react(toEvent: eventId, emoji: emoji) + } else if actionIdentifier == "chat.nio.reaction.msg" { + guard let room = room else { + return + } + if let textResponse = response as? UNTextInputNotificationResponse { + let text = textResponse.userText + + do { + // TODO: parse markdown to html + let replyContent = try await room.createReply(toEventId: eventId, text: text) + await room.sendEvent(.roomMessage, content: replyContent) + } catch { + print("could not reply to event: \(error.localizedDescription)") + } + // TODO: proper reply + //await room?.send(text: text) + } + } else { + print("unknown actionIdentifier: \(actionIdentifier)") + } + + + return + } + +} + +extension AppDelegate { + func createMessageActions(notificationCenter: UNUserNotificationCenter) { + let likeAction = UNNotificationAction(identifier: "chat.nio.reaction.emoji.like", title: "like", options: [], icon: UNNotificationActionIcon(systemImageName: "hand.thumbsup")) + // TODO: is this destructive?? + let dislikeAction = UNNotificationAction(identifier: "chat.nio.reaction.emoji.dislike", title: "dislike", options: [], icon: UNNotificationActionIcon(systemImageName: "hand.thumbsdown")) + + // TODO: textinput + let textInputAction = UNTextInputNotificationAction(identifier: "chat.nio.reaction.msg", title: "Message", options: .authenticationRequired, icon: UNNotificationActionIcon(systemImageName: "text.bubble"), textInputButtonTitle: "Reply", textInputPlaceholder: "Message") + + // TODO: intentIdentifiers + let messageReplyAction = UNNotificationCategory(identifier: "chat.nio.messageReplyAction", actions: [likeAction, dislikeAction, textInputAction], intentIdentifiers: [], options: [.allowInCarPlay, .hiddenPreviewsShowTitle, .hiddenPreviewsShowSubtitle]) + + notificationCenter.setNotificationCategories([messageReplyAction]) + } } diff --git a/Nio/Info.plist b/Nio/Info.plist index 4ac9798e..1fa16727 100644 --- a/Nio/Info.plist +++ b/Nio/Info.plist @@ -2,8 +2,6 @@ - NSSiriUsageDescription - Send Messages via Siri AppGroup $(APPGROUP) CFBundleDevelopmentRegion @@ -66,14 +64,12 @@ $(CURRENT_PROJECT_VERSION) DevelopmentTeam $(DEVELOPMENT_TEAM) - INIntentsSupported - - INSendMessageIntent - LSApplicationCategoryType public.app-category.social-networking LSRequiresIPhoneOS + NSSiriUsageDescription + Send Messages via Siri NSUserActivityTypes chat.nio.chat @@ -83,6 +79,8 @@ UIApplicationSupportsMultipleScenes + UIBackgroundModes + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -102,9 +100,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UTImportedTypeDeclarations - - - diff --git a/Nio/Nio.entitlements b/Nio/Nio.entitlements index 94894ca9..c0eefae2 100644 --- a/Nio/Nio.entitlements +++ b/Nio/Nio.entitlements @@ -2,12 +2,16 @@ + aps-environment + development com.apple.developer.associated-domains https://matrix.to com.apple.developer.siri + com.apple.developer.usernotifications.communication + com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/Nio/NioApp.swift b/Nio/NioApp.swift index 9675da5d..4949cf47 100644 --- a/Nio/NioApp.swift +++ b/Nio/NioApp.swift @@ -10,7 +10,7 @@ struct NioApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate #endif - @StateObject private var accountStore = AccountStore() + @StateObject private var accountStore = AccountStore.shared //@State private var selectedRoomId: ObjectIdentifier? diff --git a/Nio/Settings/SettingsContainerView.swift b/Nio/Settings/SettingsContainerView.swift index 68b50524..6340e93f 100644 --- a/Nio/Settings/SettingsContainerView.swift +++ b/Nio/Settings/SettingsContainerView.swift @@ -34,14 +34,18 @@ private struct SettingsView: View { @Environment(\.presentationMode) private var presentationMode /// Show a info banner for e.g. changing the developer setting - func showInfoBanner(_ text: String, identifier: String) { + func showInfoBanner(_ text: String, body: String? = nil, identifier: String) { + // TODO: fallback if notifications is disabled print("trying to show banner") asyncDetached { let notification = UNMutableNotificationContent() - notification.body = text + notification.title = text + if let body = body { + notification.body = body + } notification.sound = UNNotificationSound.default notification.userInfo = ["settings": identifier] - notification.title = "Settings changed" + //notification.title = "Settings changed" notification.badge = await UIApplication.shared.applicationIconBadgeNumber as NSNumber let request = UNNotificationRequest(identifier: identifier, content: notification, trigger: nil) @@ -101,6 +105,25 @@ private struct SettingsView: View { }) { Text("delete sk items") } + + Button(action: { + async { + do { + try await AccountStore.shared.setPusher() + showInfoBanner("Pusher repushed", identifier: "chat.nio.developer-settings.reset-pusher") + } catch { + print("failed to reset pusher") + showInfoBanner("Pusher update failed", body: error.localizedDescription, identifier: "chat.nio.developer-settings.reset-pusher") + } + } + }) { + Text("refresh pusher config") + } + + Text("\(AccountStore.shared.session?.crypto.crossSigning.state.rawValue ?? -1)") + .onTapGesture { + AccountStore.shared.session?.crypto.crossSigning.refreshState(success: nil, failure: nil) + } } } } diff --git a/NioIntentsExtension/Info.plist b/NioIntentsExtension/Info.plist index 213ecc0d..b4228b96 100644 --- a/NioIntentsExtension/Info.plist +++ b/NioIntentsExtension/Info.plist @@ -2,26 +2,29 @@ - CFBundleVersion - $(CURRENT_PROJECT_VERSION) + NSUserActivityTypes + + chat.nio.chat + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) NSExtension NSExtensionAttributes IntentsRestrictedWhileLocked - INSendMessageIntent INSearchForMessagesIntent INSetMessageAttributeIntent IntentsRestrictedWhileProtectedDataUnavailable - INSendMessageIntent INSearchForMessagesIntent INSetMessageAttributeIntent IntentsSupported + INSendMessage INSendMessageIntent INSearchForMessagesIntent INSetMessageAttributeIntent diff --git a/NioIntentsExtensionUI/Info.plist b/NioIntentsExtensionUI/Info.plist index 62c8c8a2..7c5fc233 100644 --- a/NioIntentsExtensionUI/Info.plist +++ b/NioIntentsExtensionUI/Info.plist @@ -2,8 +2,10 @@ - CFBundleVersion - $(CURRENT_PROJECT_VERSION) + NSUserActivityTypes + + chat.nio.chat + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) NSExtension diff --git a/NioIntentsExtensionUI/IntentViewController.swift b/NioIntentsExtensionUI/IntentViewController.swift index c6b772f2..10e0531e 100644 --- a/NioIntentsExtensionUI/IntentViewController.swift +++ b/NioIntentsExtensionUI/IntentViewController.swift @@ -17,6 +17,8 @@ import IntentsUI class IntentViewController: UIViewController, INUIHostedViewControlling { + @IBOutlet var label: UILabel? + override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. diff --git a/NioKit/Extensions/MX+Identifiable.swift b/NioKit/Extensions/MX+Identifiable.swift index 05e88fff..8f12c9fc 100644 --- a/NioKit/Extensions/MX+Identifiable.swift +++ b/NioKit/Extensions/MX+Identifiable.swift @@ -29,7 +29,6 @@ public extension MXStringId { // Literals } public extension MXStringId { - init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.init(try container.decode(String.self)) @@ -62,4 +61,18 @@ extension MXRoom: Identifiable { } } } -extension MXEvent: Identifiable {} +extension MXEvent: Identifiable { + public struct MXEventId: MXStringId, Hashable { + public var id: String + + public init(_ id: String) { + self.id = id + } + } + + public var id: MXEventId { + get { + return MXEventId(self.eventId) + } + } +} diff --git a/NioKit/Extensions/MXRestClient+Async.swift b/NioKit/Extensions/MXRestClient+Async.swift index fda623da..72069f6b 100644 --- a/NioKit/Extensions/MXRestClient+Async.swift +++ b/NioKit/Extensions/MXRestClient+Async.swift @@ -29,4 +29,43 @@ extension MXRestClient { self.wellKnow({ continuation.resume(returning: $0!) }, failure: { continuation.resume(throwing: $0!) }) } } + + func pushers() async throws -> [MXPusher] { + try await withCheckedThrowingContinuation { continuation in + self.pushers({ continuation.resume(returning: $0 ?? []) }, failure: { continuation.resume(throwing: $0!) }) + } + } + + + func setPusher(puskKey: String, kind: MXPusherKind, appId: String, appDisplayName: String, deviceDisplayName: String, profileTag: String, lang: String, data: [String: Any], append: Bool) async throws { + return try await withCheckedThrowingContinuation { continuation in + self.setPusher(pushKey: puskKey, kind: kind, appId: appId, appDisplayName: appDisplayName, deviceDisplayName: deviceDisplayName, profileTag: profileTag, lang: lang, data: data, append: append, completion: {resp in + switch resp { + case let .success(v): + continuation.resume(returning: v) + case let .failure(e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } + + public func event(withEventId event: MXEvent.MXEventId, inRoom room: MXRoom.MXRoomId) async throws -> MXEvent { + return try await withCheckedThrowingContinuation { continuation in + self.event(withEventId: event.id, inRoom: room.id, completion: {resp in + switch resp { + case let .success(v): + continuation.resume(returning: v) + case let .failure(e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } + //func event(withEventId: eventId.id, inRoom: <#T##String#>, completion: <#T##(MXResponse) -> Void#>) + } diff --git a/NioKit/Extensions/MXRoom+Async.swift b/NioKit/Extensions/MXRoom+Async.swift index 60ab0af1..24ccad9d 100644 --- a/NioKit/Extensions/MXRoom+Async.swift +++ b/NioKit/Extensions/MXRoom+Async.swift @@ -63,4 +63,20 @@ extension MXRoom { }) } } + + @discardableResult + func sendEvent(_ eventType: MXEventType, content: [String: Any], localEcho: inout MXEvent?) async throws -> String? { + return try await withCheckedThrowingContinuation { continuation in + self.sendEvent(eventType, content: content, localEcho: &localEcho, completion: { resp in + switch resp { + case let .success(v): + continuation.resume(returning: v) + case let .failure(e): + continuation.resume(throwing: e) + @unknown default: + continuation.resume(throwing: NioUnknownContinuationSwitchError(value: resp)) + } + }) + } + } } diff --git a/NioKit/Extensions/MXRoomMember+INPerson.swift b/NioKit/Extensions/MXRoomMember+INPerson.swift index 6cd27c6e..44df9a7c 100644 --- a/NioKit/Extensions/MXRoomMember+INPerson.swift +++ b/NioKit/Extensions/MXRoomMember+INPerson.swift @@ -10,12 +10,49 @@ import Intents import MatrixSDK extension MXRoomMember { + public var avatarUrlAbsolute: URL? { + get async { + guard let avatar = (self.avatarUrl ?? nil) else { + return nil + } + + if avatar.starts(with: "http") { + return URL(string: avatar) + } + + if let mxUrl = await AccountStore.shared.session?.mediaManager.url(ofContent: avatar), + let url = URL(string: mxUrl) { + return url + } else { + return URL(string: avatar) + } + } + } + var inPerson: INPerson { - let inImage = self.avatarUrl.flatMap { avatar in - URL(string: avatar) - }.flatMap { url in - INImage(url: url) + get async { + //let imageUrl = await self.avatarUrlAbsolute + let imageUrl = URL(string: "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png") + let inImage = imageUrl.flatMap({ INImage(url: $0) }) + //let inImage = imageUrl.flatMap({ INImage(url: $0, width: 50, height: 50) }) + + return INPerson( + personHandle: INPersonHandle(value: self.userId, type: .unknown), + nameComponents: nil, // TODO + displayName: self.displayname, + image: inImage, + contactIdentifier: nil, + customIdentifier: self.userId, + isMe: false, + suggestionType: .instantMessageAddress + ) } + } + + func inPerson(isMe: Bool = false) async -> INPerson { + let imageUrl = URL(string: "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png") + let inImage = imageUrl.flatMap({ INImage(url: $0) }) + return INPerson( personHandle: INPersonHandle(value: self.userId, type: .unknown), nameComponents: nil, // TODO @@ -23,7 +60,7 @@ extension MXRoomMember { image: inImage, contactIdentifier: nil, customIdentifier: self.userId, - isMe: false, + isMe: isMe, suggestionType: .instantMessageAddress ) } @@ -31,6 +68,13 @@ extension MXRoomMember { extension MXRoomMembers { var inPerson: [INPerson] { - self.members.map { $0.inPerson } + get async { + //await self.members.map { await $0.inPerson } + var inPerson: [INPerson] = [] + for member in self.members { + inPerson.append(await member.inPerson) + } + return inPerson + } } } diff --git a/NioKit/Extensions/MXSession+Async.swift b/NioKit/Extensions/MXSession+Async.swift index dc5e51f6..d40a8236 100644 --- a/NioKit/Extensions/MXSession+Async.swift +++ b/NioKit/Extensions/MXSession+Async.swift @@ -53,6 +53,14 @@ extension MXSession { } } } + + //store.session?.event(withEventId: <#T##String!#>, inRoom: <#T##String!#>, success: <#T##((MXEvent?) -> Void)!##((MXEvent?) -> Void)!##(MXEvent?) -> Void#>, failure: <#T##((Error?) -> Void)!##((Error?) -> Void)!##(Error?) -> Void#>) + + public func event(withEventId event: MXEvent.MXEventId, inRoom room: MXRoom.MXRoomId) async throws -> MXEvent? { + return try await withCheckedThrowingContinuation {continuation in + self.event(withEventId: event.id, inRoom: room.id, success: { continuation.resume(returning: $0) }, failure: { continuation.resume(throwing: $0!) }) + } + } } struct NioUnknownContinuationSwitchError: Error { diff --git a/NioKit/Models/Custom Events/ReplyEvent.swift b/NioKit/Models/Custom Events/ReplyEvent.swift new file mode 100644 index 00000000..f9e1cf06 --- /dev/null +++ b/NioKit/Models/Custom Events/ReplyEvent.swift @@ -0,0 +1,85 @@ +// +// ReplyEvent.swift +// Nio +// +// Created by Finn Behrens on 19.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Foundation +import MatrixSDK + +struct ReplyEvent { + let eventId: MXEvent.MXEventId + let roomId: MXRoom.MXRoomId + let sender: String + let text: String + let textHtml: String? + let replyText: String? + let replyTextHtml: String? + + init(eventId: MXEvent.MXEventId, roomId: MXRoom.MXRoomId, sender: String, text: String, textHtml: String? = nil, replyText: String?, replyTextHtml: String? = nil) { + self.eventId = eventId + self.roomId = roomId + self.sender = sender + self.text = text + self.textHtml = textHtml + self.replyText = replyText + self.replyTextHtml = replyTextHtml + } + +} + +extension ReplyEvent: CustomEvent { + func encodeContent() throws -> [String: Any] { + let replyText = self.replyTextHtml ?? self.replyText ?? "" + let text = self.textHtml ?? self.text + + let bodyText: String + if let replyText = self.replyText { + bodyText = "> " + replyText + "\n" + self.text + } else { + bodyText = self.text + } + + // TODO: via in roomId.getMatrixToLink + let formattedBody = "
In reply to
\(replyText)
\(text)" + + + let content: [String: Any] = [ + "msgtype": kMXMessageTypeText, + "body": bodyText, + "format": kMXRoomMessageFormatHTML, + "formatted_body": formattedBody, + "m.relates_to": [ + "m.in_reply_to": [ + "event_id": eventId.id + ] + ] + ] + + + return content + } +} + +extension MXEvent.MXEventId { + func getMatrixToLink(_ roomId: MXRoom.MXRoomId) -> String { + return "https://matrix.to/#/\(roomId.id)/\(self.id)" + } +} + +extension MXRoom.MXRoomId { + func getMatrixToLink() -> String { + return "https://matrix.to/#/\(self.id)" + } +} + +extension MXEvent { + func createReply(text: String, htmlText: String? = nil) -> ReplyEvent { + let body = self.content["body"] as? String + let formattedBody = self.content["formatted_body"] as? String + + return ReplyEvent(eventId: self.id, roomId: MXRoom.MXRoomId(self.roomId), sender: self.sender, text: text, textHtml: htmlText, replyText: body, replyTextHtml: formattedBody) + } +} diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index d39aa72e..1536d62c 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -104,10 +104,86 @@ public class NIORoom: ObservableObject { self.eventCache.insert(event, at: 0) case .forwards: self.eventCache.append(event) + self.donateNotification(event: event) @unknown default: assertionFailure("Unknown direction value") } } + + public func donateNotification(event: MXEvent) { + guard event.type == "m.room.message" else { + return + } + if event.timestamp.distance(to: Date()) >= -100 { + print("skipping? \(String(describing: event.eventId))") + return + } + async { + do { + let intent = try await self.createIntent(event: event) + let interaction = try await self.createNotification(event: event, messageIntent: intent) + + try await interaction.donate() + } catch { + Self.logger.warning("could not donate intent for add: \(error.localizedDescription)") + } + } + } + + public func createIntent(event: MXEvent) async throws -> INSendMessageIntent { + let members = try await self.room.members()?.members ?? [] + //let recipients = await members.filter({ $0.userId != event.sender }).map({ $0.inPerson }) + var recipients: [INPerson] = [] + for recipient in members.filter({ $0.userId != event.sender}) { + recipients.append( await recipient.inPerson(isMe: recipient.userId == AccountStore.shared.credentials?.userId )) + } + + print("sender") + //let sender = await members.filter({ $0.userId == event.sender }).first?.inPerson + let senderMember = await members.filter({ $0.userId == event.sender }).first + let sender = await senderMember?.inPerson() + + let body = event.content["body"] as? String + + let messageIntent = INSendMessageIntent( + recipients: recipients, + outgoingMessageType: .outgoingMessageText, + content: body, + speakableGroupName: self.isDirect ? nil : INSpeakableString(spokenPhrase: room.summary.displayname), + conversationIdentifier: room.id.id, + serviceName: "matrix", + sender: sender, + attachments: nil) + + return messageIntent + } + + public func createNotification(event: MXEvent, messageIntent: INSendMessageIntent) async throws -> INInteraction { + guard let selfId = AccountStore.shared.credentials?.userId else { + throw AccountStoreError.noCredentials + } + let isMe = event.sender == selfId + + let userActivity = NSUserActivity(activityType: "chat.nio.chat") + userActivity.isEligibleForHandoff = true + userActivity.isEligibleForSearch = true + userActivity.isEligibleForPrediction = true + userActivity.title = self.displayName + userActivity.userInfo = ["id": self.id.rawValue as String] + + let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String) + + attributes.contentDescription = "Open chat with \(self.displayName)" + attributes.instantMessageAddresses = [ self.room.roomId ] + userActivity.contentAttributeSet = attributes + userActivity.webpageURL = URL(string: "https://matrix.to/#/\(self.room.roomId ?? "")") + + let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) + let interaction = INInteraction(intent: messageIntent, response: response) + interaction.direction = isMe ? INInteractionDirection.outgoing : INInteractionDirection.incoming + interaction.dateInterval = DateInterval(start: event.timestamp, duration: 0) + return interaction + } public func events() -> EventCollection { return EventCollection(eventCache + room.outgoingMessages()) @@ -131,17 +207,29 @@ public class NIORoom: ObservableObject { await self.donateOutgoingIntent(text) } + public func react(toEvent event: MXEvent.MXEventId, emoji: String) async { + let content = try! ReactionEvent(eventId: event.id, key: emoji).encodeContent() + + await self.sendEvent(.reaction, content: content) + } + + @available(*, deprecated, message: "Prefer MXEvent.MXEventId methode") public func react(toEventId eventId: String, emoji: String) { - // swiftlint:disable:next force_try - let content = try! ReactionEvent(eventId: eventId, key: emoji).encodeContent() - - objectWillChange.send() // room.outgoingMessages() will change - var localEcho: MXEvent? = nil - // TODO: async - room.sendEvent(.reaction, content: content, localEcho: &localEcho) { _ in - self.objectWillChange.send() // localEcho.sentState has(!) changed + async { + await self.react(toEvent: MXEvent.MXEventId(eventId), emoji: emoji) } } + + public func sendEvent(_ eventType: MXEventType, content: [String: Any]) async { + var localEcho: MXEvent? + + do { + try await room.sendEvent(eventType, content: content, localEcho: &localEcho) + } catch { + Self.logger.warning("could not send \(eventType.identifier): \(error.localizedDescription)") + } + self.objectWillChange.send() + } public func edit(text: String, eventId: String) { guard !text.isEmpty else { return } @@ -180,6 +268,20 @@ public class NIORoom: ObservableObject { await self.donateOutgoingIntent() } + + public func createReply(toEventId eventId: MXEvent.MXEventId, text: String, htmlText: String? = nil) async throws -> [String : Any] { + let event = try await AccountStore.shared.session?.event(withEventId: eventId, inRoom: self.id) + + guard let event = event else { + throw AccountStoreError.noSessionOpened + } + + return try self.createReply(toEvent: event, text: text, htmlText: htmlText) + } + + public func createReply(toEvent event: MXEvent, text: String, htmlText: String? = nil) throws -> [String: Any] { + return try event.createReply(text: text, htmlText: htmlText).encodeContent() + } public func markAllAsRead() { room.markAllAsRead() @@ -191,11 +293,18 @@ public class NIORoom: ObservableObject { } // intent + @available(*, deprecated, message: "Prefer `createNotification` to create an intent and donate that response") private func donateOutgoingIntent(_ text: String? = nil) async { do { - let recipients = try await self.room.members()?.inPerson + let senderId = room.mxSession.credentials.userId + //let recipients = try await self.room.members()?.members.filter({ $0.userId != senderId }).map({$0.inPerson}) + let members = try await self.room.members()?.members.filter({ $0.userId != senderId }) ?? [] + var recipients: [INPerson] = [] + for recipient in members { + recipients.append( await recipient.inPerson) + } - let senderPersonHandle = INPersonHandle(value: room.mxSession.credentials.userId, type: .unknown) + let senderPersonHandle = INPersonHandle(value: senderId, type: .unknown) let sender = INPerson( personHandle: senderPersonHandle, nameComponents: nil, @@ -292,6 +401,10 @@ public class NIORoom: ObservableObject { Self.logger.warning("could not bootstrap pagination") } } + + public func findEvent(id: MXEvent.MXEventId) -> MXEvent? { + self.eventCache.filter({ $0.id == id }).first + } } extension NIORoom: Identifiable { diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 173c15cf..740c60db 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -10,12 +10,23 @@ public enum LoginState { case authenticating case failure(Error) case loggedIn(userId: String) + + public var isAuthenticating: Bool { + switch self { + case .authenticating: + return true + default: + return false + } + } } @MainActor public class AccountStore: ObservableObject { static let logger = Logger(subsystem: "chat.nio.chat", category: "AccountStore") + public static let shared = AccountStore() + public var client: MXRestClient? public var session: MXSession? @@ -40,6 +51,10 @@ public class AccountStore: ObservableObject { await Self.deleteSkItems() } } + + let developmentTeam = Bundle.main.infoDictionary?["DevelopmentTeam"] as? String + print("team: ") + print(developmentTeam) Configuration.setupMatrixSDKSettings() guard let credentials = MXCredentials.from(keychain) else { @@ -82,6 +97,8 @@ public class AccountStore: ObservableObject { credentials.save(to: keychain) loginState = try await sync() session?.crypto.warnOnUnknowDevices = false + + try await self.setPusher() } catch { loginState = .failure(error) } @@ -92,6 +109,7 @@ public class AccountStore: ObservableObject { do { try await session?.logout() + try await self.setPusher(enable: false) loginState = .loggedOut } catch { // Close the session even if the logout request failed @@ -169,6 +187,10 @@ public class AccountStore: ObservableObject { updateUserDefaults(with: rooms) return rooms } + + public func findRoom(id: MXRoom.MXRoomId) -> NIORoom? { + return self.rooms.filter({ $0.id == id }).first + } private func updateUserDefaults(with rooms: [NIORoom]) { let roomItems = rooms.map { RoomItem(room: $0.room) } @@ -197,10 +219,58 @@ public class AccountStore: ObservableObject { } // MARK: - Push Notifications - /* - func requestNotificationToken() { + internal var pushKey: String? + + public func setPusher(key: Data, enable: Bool = true) async throws { + let base = key.base64EncodedString() + try await setPusher(stringKey: base, enable: enable) + } + + public func setPusher(stringKey: String, enable: Bool = true) async throws { + if pushKey != nil { + Self.logger.warning("Pushkey already set to \(self.pushKey!)") + } + self.pushKey = stringKey - }*/ + try await setPusher(enable: enable) + } + + /// function is also used to reset the push config + // TODO: lang, dynamic pusher + public func setPusher(enable: Bool = true) async throws { + guard let session = self.session else { + throw AccountStoreError.noSessionOpened + } + guard let pushKey = self.pushKey else { + throw AccountStoreError.noPuskKey + } + + let appId = Bundle.main.bundleIdentifier ?? "nio.chat" + let lang = NSLocale.preferredLanguages.first ?? "en-US" + + let data: [String : Any] = [ + "url": "https://dev.matrix-push.kloenk.dev/_matrix/push/v1/notify", + "format": "event_id_only", + "default_payload": [ + "aps": [ + "mutable-content": 1, + "content-available": 1, + "alert": [ + "loc-key": "MESSAGE", + "loc-args": [], + ] + ] + ] + ]; + + let pushers = try await session.matrixRestClient.pushers() + if pushers.count != 0 { + Self.logger.debug("got pushers: \(String(describing: pushers))") + } + try await session.matrixRestClient.setPusher(puskKey: pushKey, kind: enable ? .http : .none, appId: appId, appDisplayName: "Nio", deviceDisplayName: "NioiOS", profileTag: "gloaaabal", lang: lang, data: data, append: false) + //session.matrixRestClient.setPusher(pushKey: key, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) + //self.session?.matrixRestClient.setPusher(pushKey: <#T##String#>, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) + } /*func setPusher() { self.session?.matrixRestClient.setPusher(pushKey: <#T##String#>, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) }*/ @@ -208,5 +278,7 @@ public class AccountStore: ObservableObject { enum AccountStoreError: Error { case noCredentials + case noSessionOpened case invalidUrl + case noPuskKey } diff --git a/NioNSE/Info.plist b/NioNSE/Info.plist new file mode 100644 index 00000000..05e40724 --- /dev/null +++ b/NioNSE/Info.plist @@ -0,0 +1,19 @@ + + + + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + AppGroup + $(APPGROUP) + DevelopmentTeam + $(DEVELOPMENT_TEAM) + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/NioNSE/NioNSE.entitlements b/NioNSE/NioNSE.entitlements new file mode 100644 index 00000000..7d4daac6 --- /dev/null +++ b/NioNSE/NioNSE.entitlements @@ -0,0 +1,15 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + keychain-access-groups + + $(AppIdentifierPrefix)nio.keychain + $(AppIdentifierPrefix)chat.nio.credentials + + + diff --git a/NioNSE/NioNSEDebug.entitlements b/NioNSE/NioNSEDebug.entitlements new file mode 100644 index 00000000..b262e50c --- /dev/null +++ b/NioNSE/NioNSEDebug.entitlements @@ -0,0 +1,19 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.$(APPGROUP) + + com.apple.security.network.client + + keychain-access-groups + + $(AppIdentifierPrefix)nio.keychain + $(AppIdentifierPrefix)chat.nio.credentials + + + diff --git a/NioNSE/NotificationService.swift b/NioNSE/NotificationService.swift new file mode 100644 index 00000000..9ee694e8 --- /dev/null +++ b/NioNSE/NotificationService.swift @@ -0,0 +1,136 @@ +// +// NotificationService.swift +// NioNSE +// +// Created by Finn Behrens on 18.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import UserNotifications +import NioKit +import MatrixSDK + +class NotificationService: UNNotificationServiceExtension { + + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + var contentIntent: UNNotificationContent? + + @MainActor + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + print("didReceive") + print(bestAttemptContent?.userInfo as Any) + if let bestAttemptContent = bestAttemptContent { + let store = AccountStore.shared + // Modify the notification content here... + //bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" + + sleep(1) + while store.loginState.isAuthenticating { + // FIXME: !!!!!!! + #warning("this is not good coding!!!!!") + usleep(2000) + //sleep(1) + } + + //store.session?.crypto. + + sleep(1) + + let roomId = MXRoom.MXRoomId(bestAttemptContent.userInfo["room_id"] as? String ?? "") + bestAttemptContent.threadIdentifier = roomId.id + bestAttemptContent.categoryIdentifier = "chat.nio.messageReplyAction" + let eventId = MXEvent.MXEventId(bestAttemptContent.userInfo["event_id"] as? String ?? "") + let room = AccountStore.shared.findRoom(id: roomId) + bestAttemptContent.subtitle = !(room?.isDirect ?? false) ? room?.displayName ?? "" : "" + + async { + do { + //let event = try await store.session?.matrixRestClient.event(withEventId: eventId, inRoom: roomId) + let event = try await store.session?.event(withEventId: eventId, inRoom: roomId) + guard let event = event else { + print("did not find an event") + contentHandler(bestAttemptContent) + return + } + print("eventType: \(String(describing: event.type))") + print("eventContent: \(String(describing: event.content))") + print("error: \(String(describing: event.decryptionError))") + + + + if let intent = try await room?.createIntent(event: event) { + print(intent.content as Any) + print("senderImage: \(intent.sender?.image)") + print("keyImage: \(intent.keyImage())") + + bestAttemptContent.body = intent.content ?? "Message" + bestAttemptContent.title = intent.sender?.displayName ?? intent.sender?.customIdentifier ?? "" + self.contentIntent = try bestAttemptContent.updating(from: intent) + //self.contentIntent = try request.content.updating(from: intent) as! UNMutableNotificationContent + //self.contentIntent?.body = intent.content ?? "MESSAGE" + + + print("creatent contentIntent") + print(contentIntent!.body) + print(contentIntent as Any) + if let interaction = try await room?.createNotification(event: event, messageIntent: intent) { + try await interaction.donate() + } + } else { + print("did not get an intent") + } + } catch { + print("error") + print(error.localizedDescription) + } + + print("returning event") + if let contentIntent = contentIntent { + print("found contentIntent") + contentHandler(contentIntent) + } else { + print("bestAttemptContent") + contentHandler(bestAttemptContent) + } + // TODO: exit or not? + exit(0) + } + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler { + if let contentIntent = contentIntent { + contentHandler(contentIntent) + } + else if let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + } + + /* + override func viewDidLoad() { + print("viewDidLoad") + super.viewDidLoad() + // Do any required interface initialization here. + } + + func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + print("didReceive request") + //contentHandler() + } + + func didReceive(_ notification: UNNotification) { + print("didReceive") + self.label?.text = notification.request.content.body + } + */ + +} From ed54ab5159c09b34d046f95f33f1aaf52d4a9692 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Tue, 22 Jun 2021 16:12:59 +0200 Subject: [PATCH 06/11] add Siri intent --- Nio.xcodeproj/project.pbxproj | 47 +++++- .../xcschemes/NioIntentsExtension.xcscheme | 4 +- Nio/AppDelegate.swift | 1 + Nio/Info.plist | 1 + NioIntentsExtension/Info.plist | 14 +- NioIntentsExtension/IntentHandler.swift | 147 ++++++++++++++---- .../NioIntentsExtension.entitlements | 8 + NioIntentsExtensionUI/Info.plist | 8 +- .../NioIntentsExtensionUI.entitlements | 2 + NioKit/Models/NIORoom.swift | 39 ++++- NioKit/Session/AccountStore.swift | 9 +- 11 files changed, 230 insertions(+), 50 deletions(-) diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 5ed70b62..417d965a 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -101,6 +101,9 @@ 95CAC88C26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */; }; 95CAC88E26791D11001D71DC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88D26791D11001D71DC /* AppDelegate.swift */; }; 95CAC89026792247001D71DC /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC88F26792247001D71DC /* Intents.intentdefinition */; }; + 95EF1D78267F7B94000FAEF0 /* libNioKit-iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8302D4C25F26CA500E962E9 /* libNioKit-iOS.a */; }; + 95EF1D7C267F7B98000FAEF0 /* MatrixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 95EF1D7B267F7B98000FAEF0 /* MatrixSDK */; }; + 95EF1D7E267F7C7C000FAEF0 /* CommonMarkAttributedString in Frameworks */ = {isa = PBXBuildFile; productRef = 95EF1D7D267F7C7C000FAEF0 /* CommonMarkAttributedString */; }; A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */; }; A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51F762B25D6E9950061B4A4 /* MessageTextViewWrapper.swift */; }; A58352A625A667AB00533363 /* MatrixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A58352A525A667AB00533363 /* MatrixSDK */; }; @@ -267,6 +270,13 @@ remoteGlobalIDString = 95CAC86626791295001D71DC; remoteInfo = NioIntentsExtension; }; + 95EF1D79267F7B94000FAEF0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39C931D123843289004449E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8302D4B25F26CA500E962E9; + remoteInfo = "NioKit-iOS"; + }; CADF663924614D2300F5063F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 39C931D123843289004449E1 /* Project object */; @@ -540,6 +550,9 @@ buildActionMask = 2147483647; files = ( 95CAC86926791295001D71DC /* Intents.framework in Frameworks */, + 95EF1D7E267F7C7C000FAEF0 /* CommonMarkAttributedString in Frameworks */, + 95EF1D7C267F7B98000FAEF0 /* MatrixSDK in Frameworks */, + 95EF1D78267F7B94000FAEF0 /* libNioKit-iOS.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1193,8 +1206,13 @@ buildRules = ( ); dependencies = ( + 95EF1D7A267F7B94000FAEF0 /* PBXTargetDependency */, ); name = NioIntentsExtension; + packageProductDependencies = ( + 95EF1D7B267F7B98000FAEF0 /* MatrixSDK */, + 95EF1D7D267F7C7C000FAEF0 /* CommonMarkAttributedString */, + ); productName = NioIntentsExtension; productReference = 95CAC86726791295001D71DC /* NioIntentsExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -1212,6 +1230,8 @@ dependencies = ( ); name = NioIntentsExtensionUI; + packageProductDependencies = ( + ); productName = NioIntentsExtensionUI; productReference = 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */; productType = "com.apple.product-type.app-extension"; @@ -1830,6 +1850,11 @@ target = 95CAC86626791295001D71DC /* NioIntentsExtension */; targetProxy = 95CAC88026791295001D71DC /* PBXContainerItemProxy */; }; + 95EF1D7A267F7B94000FAEF0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E8302D4B25F26CA500E962E9 /* NioKit-iOS */; + targetProxy = 95EF1D79267F7B94000FAEF0 /* PBXContainerItemProxy */; + }; CADF663A24614D2300F5063F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 39C931D823843289004449E1 /* Nio */; @@ -2233,7 +2258,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtension; INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtension; INFOPLIST_KEY_CFBundleName = NioIntentsExtension; - INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; + INFOPLIST_KEY_CFBundleVersion = 33; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2242,7 +2267,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.kloenk.nio.iOS.NioIntents; + PRODUCT_BUNDLE_IDENTIFIER = "$(INTENTS_EXTENSION_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -2264,7 +2289,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtension; INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtension; INFOPLIST_KEY_CFBundleName = NioIntentsExtension; - INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; + INFOPLIST_KEY_CFBundleVersion = 33; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2273,7 +2298,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.kloenk.nio.iOS.NioIntents; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -2295,7 +2320,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtensionUI; INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtensionUI; INFOPLIST_KEY_CFBundleName = NioIntentsExtensionUI; - INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; + INFOPLIST_KEY_CFBundleVersion = 33; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2326,7 +2351,7 @@ INFOPLIST_KEY_CFBundleDisplayName = NioIntentsExtensionUI; INFOPLIST_KEY_CFBundleExecutable = NioIntentsExtensionUI; INFOPLIST_KEY_CFBundleName = NioIntentsExtensionUI; - INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)"; + INFOPLIST_KEY_CFBundleVersion = 33; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Kilian Koeltzsch. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2664,6 +2689,16 @@ package = 39E5032124EAFA8700FED642 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; + 95EF1D7B267F7B98000FAEF0 /* MatrixSDK */ = { + isa = XCSwiftPackageProductDependency; + package = A58352A425A667AB00533363 /* XCRemoteSwiftPackageReference "MatrixSDK" */; + productName = MatrixSDK; + }; + 95EF1D7D267F7C7C000FAEF0 /* CommonMarkAttributedString */ = { + isa = XCSwiftPackageProductDependency; + package = CAF2AE9B245DF4B400D84133 /* XCRemoteSwiftPackageReference "CommonMarkAttributedString" */; + productName = CommonMarkAttributedString; + }; A58352A525A667AB00533363 /* MatrixSDK */ = { isa = XCSwiftPackageProductDependency; package = A58352A425A667AB00533363 /* XCRemoteSwiftPackageReference "MatrixSDK" */; diff --git a/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtension.xcscheme b/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtension.xcscheme index 6548c85b..30a920bb 100644 --- a/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtension.xcscheme +++ b/Nio.xcodeproj/xcshareddata/xcschemes/NioIntentsExtension.xcscheme @@ -47,7 +47,7 @@ Send Messages via Siri NSUserActivityTypes + INSendMessageIntent chat.nio.chat UIApplicationSceneManifest diff --git a/NioIntentsExtension/Info.plist b/NioIntentsExtension/Info.plist index b4228b96..92b68e9d 100644 --- a/NioIntentsExtension/Info.plist +++ b/NioIntentsExtension/Info.plist @@ -2,10 +2,10 @@ - NSUserActivityTypes - - chat.nio.chat - + DevelopmentTeam + $(DEVELOPMENT_TEAM) + AppGroup + $(APPGROUP) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) NSExtension @@ -24,9 +24,9 @@
IntentsSupported + INSearchForMessagesIntent INSendMessage INSendMessageIntent - INSearchForMessagesIntent INSetMessageAttributeIntent
@@ -35,5 +35,9 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).IntentHandler
+ NSUserActivityTypes + + chat.nio.chat +
diff --git a/NioIntentsExtension/IntentHandler.swift b/NioIntentsExtension/IntentHandler.swift index 7aa906a3..5d9afde4 100644 --- a/NioIntentsExtension/IntentHandler.swift +++ b/NioIntentsExtension/IntentHandler.swift @@ -7,6 +7,11 @@ // import Intents +import CoreSpotlight +import CoreServices + +import NioKit +import MatrixSDK // As an example, this class is set up to handle Message intents. // You will want to replace this or add other intents as appropriate. @@ -29,15 +34,63 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo // MARK: - INSendMessageIntentHandling // Implement resolution methods to provide additional information about your intent (optional). - public func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) { - if let recipients = intent.recipients { - - // If no recipients were provided we'll need to prompt for a value. - if recipients.count == 0 { - completion([INSendMessageRecipientResolutionResult.needsValue()]) - return + @MainActor + public func resolveRecipients(for intent: INSendMessageIntent) async -> [INSendMessageRecipientResolutionResult] { + guard let recipients = intent.recipients, + recipients.count != 0 + else { + return [INSendMessageRecipientResolutionResult.needsValue()] + } + + let store = AccountStore.shared + + //await store.loginState.waitForLogin() + //await Task.sleep(20_000) + + // TODO: wait at a better place + while await store.loginState.isAuthenticating { + // FIXME: !!!!!!! + #warning("this is not good coding!!!!!") + await Task.yield() + //print("logging in") + //sleep(1) + } + + var resolutionResults = [INSendMessageRecipientResolutionResult]() + + print("group name: \(intent.speakableGroupName)") + + for recipient in recipients { + print("handle: \(String(describing: recipient.personHandle))") + print("searching for room: \(recipient.displayName)") + let rooms = store.rooms.filter { $0.displayName.lowercased() == recipient.displayName.lowercased() }.map({room in + INPerson( + personHandle: INPersonHandle(value: room.id.id, type: .unknown), + nameComponents: nil, + displayName: room.displayName, + image: room.avatarUrl.flatMap({ INImage(url: $0)}), + contactIdentifier: nil, + customIdentifier: room.id.id, + isMe: false, + suggestionType: .none) + }) + switch rooms.count { + case 2 ... Int.max: + resolutionResults += [INSendMessageRecipientResolutionResult.disambiguation(with: rooms)] + case 1: + //resolutionResults += [INSendMessageRecipientResolutionResult.confirmationRequired(with: rooms.first!)] + resolutionResults += [INSendMessageRecipientResolutionResult.success(with: rooms.first!)] + case 0: + print("did not find a room") + resolutionResults += [INSendMessageRecipientResolutionResult.unsupported(forReason: .noValidHandle)] + default: + fatalError("how can this be possible?") } - + } + return resolutionResults + + + /* var resolutionResults = [INSendMessageRecipientResolutionResult]() for recipient in recipients { let matchingContacts = [recipient] // Implement your contact matching logic here to create an array of matching contacts @@ -59,45 +112,87 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo } } - completion(resolutionResults) + //completion(resolutionResults) + return resolutionResults } else { - completion([INSendMessageRecipientResolutionResult.needsValue()]) - } + return [INSendMessageRecipientResolutionResult.needsValue()] + //completion([INSendMessageRecipientResolutionResult.needsValue()]) + }*/ } - public func resolveContent(for intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) { + public func resolveContent(for intent: INSendMessageIntent) async -> INStringResolutionResult { if let text = intent.content, !text.isEmpty { - completion(INStringResolutionResult.success(with: text)) - } else { - completion(INStringResolutionResult.needsValue()) + print("writing text: \(text)") + return INStringResolutionResult.success(with: text) } + + return INStringResolutionResult.needsValue() } - // Once resolution is completed, perform validation on the intent and provide confirmation (optional). - public func confirm(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { + public func confirm(intent: INSendMessageIntent) async -> INSendMessageIntentResponse { + print("confirm") // Verify user is authenticated and your app is ready to send a message. let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let response = INSendMessageIntentResponse(code: .ready, userActivity: userActivity) - completion(response) + return response } // Handle the completed intent (required). - public func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { + public func handle(intent: INSendMessageIntent) async -> INSendMessageIntentResponse { + print("handle INSendMessageIntent") // Implement your application logic to send a message here. + let store = AccountStore.shared + + guard let recipient = intent.recipients?.first?.customIdentifier else { + return INSendMessageIntentResponse(code: .failure, userActivity: nil) + } + let userActivity = NSUserActivity(activityType: "chat.nio.chat") + userActivity.isEligibleForSearch = true + userActivity.isEligibleForHandoff = true + userActivity.isEligibleForPrediction = true + userActivity.title = intent.recipients!.first!.displayName + userActivity.userInfo = ["id": recipient as String] + + let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String) + + attributes.contentDescription = "Open chat with \(intent.recipients!.first!.displayName)" + attributes.instantMessageAddresses = [ recipient ] + userActivity.contentAttributeSet = attributes + userActivity.webpageURL = URL(string: "https://matrix.to/#/\(recipient)") + + // TODO: wait at a better place + while await store.loginState.isAuthenticating { + // FIXME: !!!!!!! + #warning("this is not good coding!!!!!") + await Task.yield() + } + + print("intent: \(intent)") + + guard + let room = await store.findRoom(id: MXRoom.MXRoomId(recipient)), + let content = intent.content + else { + // TODO: is this the right error? + let response = INSendMessageIntentResponse(code: .failureMessageServiceNotAvailable, userActivity: userActivity) + return response + } + + await room.send(text: content, publishIntent: false) + - let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) - completion(response) + return response } // Implement handlers for each intent you wish to handle. As an example for messages, you may wish to also handle searchForMessages and setMessageAttributes. // MARK: - INSearchForMessagesIntentHandling - public func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) { + public func handle(intent: INSearchForMessagesIntent) async -> INSearchForMessagesIntentResponse { // Implement your application logic to find a message that matches the information in the intent. let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) @@ -109,17 +204,17 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo dateSent: Date(), sender: INPerson(personHandle: INPersonHandle(value: "sarah@example.com", type: .emailAddress), nameComponents: nil, displayName: "Sarah", image: nil, contactIdentifier: nil, customIdentifier: nil), recipients: [INPerson(personHandle: INPersonHandle(value: "+1-415-555-5555", type: .phoneNumber), nameComponents: nil, displayName: "John", image: nil, contactIdentifier: nil, customIdentifier: nil)] - )] - completion(response) + )] + return response } // MARK: - INSetMessageAttributeIntentHandling - public func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) { + public func handle(intent: INSetMessageAttributeIntent) async -> INSetMessageAttributeIntentResponse { // Implement your application logic to set the message attribute here. let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity) - completion(response) + return response } } diff --git a/NioIntentsExtension/NioIntentsExtension.entitlements b/NioIntentsExtension/NioIntentsExtension.entitlements index ee95ab7e..adfabe7f 100644 --- a/NioIntentsExtension/NioIntentsExtension.entitlements +++ b/NioIntentsExtension/NioIntentsExtension.entitlements @@ -4,7 +4,15 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.dev.kloenk.nio + com.apple.security.network.client + keychain-access-groups + + $(AppIdentifierPrefix)nio.keychain +
diff --git a/NioIntentsExtensionUI/Info.plist b/NioIntentsExtensionUI/Info.plist index 7c5fc233..92cac3f1 100644 --- a/NioIntentsExtensionUI/Info.plist +++ b/NioIntentsExtensionUI/Info.plist @@ -2,10 +2,6 @@ - NSUserActivityTypes - - chat.nio.chat - CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) NSExtension @@ -22,5 +18,9 @@ NSExtensionPointIdentifier com.apple.intents-ui-service + NSUserActivityTypes + + chat.nio.chat + diff --git a/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements b/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements index ee95ab7e..225aa48b 100644 --- a/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements +++ b/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements @@ -6,5 +6,7 @@ com.apple.security.network.client + keychain-access-groups + diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index 1536d62c..2f526e3c 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -110,6 +110,7 @@ public class NIORoom: ObservableObject { } } + @available(*, deprecated, message: "Prefer `createNotification` to create an intent and donate that response") public func donateNotification(event: MXEvent) { guard event.type == "m.room.message" else { return @@ -140,7 +141,7 @@ public class NIORoom: ObservableObject { print("sender") //let sender = await members.filter({ $0.userId == event.sender }).first?.inPerson - let senderMember = await members.filter({ $0.userId == event.sender }).first + let senderMember = members.filter({ $0.userId == event.sender }).first let sender = await senderMember?.inPerson() let body = event.content["body"] as? String @@ -180,6 +181,7 @@ public class NIORoom: ObservableObject { let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) let interaction = INInteraction(intent: messageIntent, response: response) + // TODO: remove? interaction.direction = isMe ? INInteractionDirection.outgoing : INInteractionDirection.incoming interaction.dateInterval = DateInterval(start: event.timestamp, duration: 0) return interaction @@ -191,7 +193,7 @@ public class NIORoom: ObservableObject { // MARK: Sending Events - public func send(text: String) async { + public func send(text: String, publishIntent: Bool = true) async { guard !text.isEmpty else { return } objectWillChange.send() // room.outgoingMessages() will change @@ -204,7 +206,22 @@ public class NIORoom: ObservableObject { // localEcho.sentState has(!) changed self.objectWillChange.send() - await self.donateOutgoingIntent(text) + if publishIntent { + guard let localEcho = localEcho else { + return + } + do { + let messageIntent = try await createIntent(event: localEcho) + let intent = try await createNotification(event: localEcho, messageIntent: messageIntent) + intent.direction = .outgoing + if !self.isDirect { + intent.groupIdentifier = localEcho.roomId + } + try await intent.donate() + } catch { + Self.logger.warning("could not donate text message to \(self.displayName): \(error.localizedDescription)") + } + } } public func react(toEvent event: MXEvent.MXEventId, emoji: String) async { @@ -265,8 +282,20 @@ public class NIORoom: ObservableObject { // localEcho.sentState has(!) changed self.objectWillChange.send() - - await self.donateOutgoingIntent() + guard let localEcho = localEcho else { + return + } + do { + let messageIntent = try await createIntent(event: localEcho) + let intent = try await createNotification(event: localEcho, messageIntent: messageIntent) + intent.direction = .outgoing + if !self.isDirect { + intent.groupIdentifier = localEcho.roomId + } + try await intent.donate() + } catch { + Self.logger.warning("could not donate image message to \(self.displayName): \(error.localizedDescription)") + } } public func createReply(toEventId eventId: MXEvent.MXEventId, text: String, htmlText: String? = nil) async throws -> [String : Any] { diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 740c60db..998472e0 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -19,6 +19,13 @@ public enum LoginState { return false } } + + public func waitForLogin() async { + while self.isAuthenticating { + print("trying to authenticate") + //await Task.sleep(20_000) + } + } } @MainActor @@ -53,8 +60,6 @@ public class AccountStore: ObservableObject { } let developmentTeam = Bundle.main.infoDictionary?["DevelopmentTeam"] as? String - print("team: ") - print(developmentTeam) Configuration.setupMatrixSDKSettings() guard let credentials = MXCredentials.from(keychain) else { From 4675d80dd9a1eef72d21c12069b70c6391f7d896 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 25 Jun 2021 12:31:15 +0200 Subject: [PATCH 07/11] move NSE onto async --- NioNSE/NotificationService.swift | 60 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/NioNSE/NotificationService.swift b/NioNSE/NotificationService.swift index 9ee694e8..25abf5f4 100644 --- a/NioNSE/NotificationService.swift +++ b/NioNSE/NotificationService.swift @@ -16,38 +16,38 @@ class NotificationService: UNNotificationServiceExtension { var bestAttemptContent: UNMutableNotificationContent? var contentIntent: UNNotificationContent? - @MainActor + + //@MainActor override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - - print("didReceive") - print(bestAttemptContent?.userInfo as Any) - if let bestAttemptContent = bestAttemptContent { - let store = AccountStore.shared - // Modify the notification content here... - //bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" - - sleep(1) - while store.loginState.isAuthenticating { - // FIXME: !!!!!!! - #warning("this is not good coding!!!!!") - usleep(2000) - //sleep(1) - } - - //store.session?.crypto. - - sleep(1) - - let roomId = MXRoom.MXRoomId(bestAttemptContent.userInfo["room_id"] as? String ?? "") - bestAttemptContent.threadIdentifier = roomId.id - bestAttemptContent.categoryIdentifier = "chat.nio.messageReplyAction" - let eventId = MXEvent.MXEventId(bestAttemptContent.userInfo["event_id"] as? String ?? "") - let room = AccountStore.shared.findRoom(id: roomId) - bestAttemptContent.subtitle = !(room?.isDirect ?? false) ? room?.displayName ?? "" : "" + async { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - async { + print("didReceive") + print(bestAttemptContent?.userInfo as Any) + if let bestAttemptContent = bestAttemptContent { + let store = AccountStore.shared + // Modify the notification content here... + //bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" + + while await store.loginState.isAuthenticating { + // FIXME: !!!!!!! + #warning("this is not good coding!!!!!") + await Task.yield() + //sleep(1) + } + + //store.session?.crypto. + + sleep(1) + + let roomId = MXRoom.MXRoomId(bestAttemptContent.userInfo["room_id"] as? String ?? "") + bestAttemptContent.threadIdentifier = roomId.id + bestAttemptContent.categoryIdentifier = "chat.nio.messageReplyAction" + let eventId = MXEvent.MXEventId(bestAttemptContent.userInfo["event_id"] as? String ?? "") + let room = await AccountStore.shared.findRoom(id: roomId) + bestAttemptContent.subtitle = await !(room?.isDirect ?? false) ? room?.displayName ?? "" : "" + do { //let event = try await store.session?.matrixRestClient.event(withEventId: eventId, inRoom: roomId) let event = try await store.session?.event(withEventId: eventId, inRoom: roomId) From 475ff14aa0514da744f409ba5bba7abbcaa09961 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 25 Jun 2021 13:45:34 +0200 Subject: [PATCH 08/11] Timeline pagination fix and SearchAble modifier --- .../RecentRoomsContainerView.swift | 15 +++++++--- Nio/Conversations/RecentRoomsView.swift | 13 +++++++-- Nio/Conversations/RoomView.swift | 16 ++++++++-- Nio/Shared Views/ReverseList.swift | 16 ++++++---- NioIntentsExtension/IntentHandler.swift | 4 +-- NioKit/Models/NIORoom.swift | 29 +++++++------------ 6 files changed, 57 insertions(+), 36 deletions(-) diff --git a/Nio/Conversations/RecentRoomsContainerView.swift b/Nio/Conversations/RecentRoomsContainerView.swift index 76eced18..c9c937b4 100644 --- a/Nio/Conversations/RecentRoomsContainerView.swift +++ b/Nio/Conversations/RecentRoomsContainerView.swift @@ -13,6 +13,7 @@ struct RecentRoomsContainerView: View { @State private var selectedNavigationItem: SelectedNavigationItem? @State private var selectedRoomId: MXRoom.MXRoomId? + @State private var searchText: String = "" private func autoselectFirstRoom() { /*if selectedRoomId == nil { @@ -31,13 +32,14 @@ struct RecentRoomsContainerView: View { var body: some View { RecentRoomsView(selectedNavigationItem: $selectedNavigationItem, selectedRoomId: $selectedRoomId, + searchText: $searchText, rooms: store.rooms) .sheet(item: $selectedNavigationItem) { NavigationSheet(selectedItem: $0, selectedRoomId: $selectedRoomId) - // This really shouldn't be necessary. SwiftUI bug? - // 2021-03-07(hh): SwiftUI doesn't document when - // environments are preserved. Also - // different between platforms. + // This really shouldn't be necessary. SwiftUI bug? + // 2021-03-07(hh): SwiftUI doesn't document when + // environments are preserved. Also + // different between platforms. .environmentObject(self.store) .accentColor(accentColor) } @@ -49,6 +51,7 @@ struct RecentRoomsContainerView: View { .onChange(of: appDelegate.selectedRoom) { newRoom in selectedRoomId = newRoom } + .searchable(text: $searchText) } } @@ -72,16 +75,20 @@ struct RoomsListSection: View { return self.rooms[leaveId] } + // we could use the userhandle incease of direct rooms here, currently we use the none readable room id + @MainActor private var sectionContent: some View { ForEach(rooms) { room in NavigationLink(destination: RoomContainerView(room: room), tag: room.id, selection: $selectedRoomId) { RoomListItemContainerView(room: room) } + //.searchCompletion(room.displayName) } .onDelete(perform: setLeaveIndex) } @ViewBuilder + @MainActor private var section: some View { if let sectionHeader = sectionHeader { Section(header: Text(sectionHeader)) { diff --git a/Nio/Conversations/RecentRoomsView.swift b/Nio/Conversations/RecentRoomsView.swift index f08fea7a..aaeb5627 100644 --- a/Nio/Conversations/RecentRoomsView.swift +++ b/Nio/Conversations/RecentRoomsView.swift @@ -16,15 +16,22 @@ struct RecentRoomsView: View { @Binding var selectedNavigationItem: SelectedNavigationItem? @Binding var selectedRoomId: MXRoom.MXRoomId? + @Binding var searchText: String let rooms: [NIORoom] private var joinedRooms: [NIORoom] { - rooms.filter {$0.room.summary.membership == .join} + rooms.filter { + $0.room.summary.membership == .join && + (searchText.isEmpty ? true : $0.displayName.lowercased().contains(searchText.lowercased())) + } } private var invitedRooms: [NIORoom] { - rooms.filter {$0.room.summary.membership == .invite} + rooms.filter { + $0.room.summary.membership == .invite && + (searchText.isEmpty ? true : $0.displayName.lowercased().contains(searchText.lowercased())) + } } private var settingsButton: some View { @@ -86,6 +93,6 @@ struct RecentRoomsView: View { struct RecentRoomsView_Previews: PreviewProvider { static var previews: some View { - RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), rooms: []) + RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), searchText: .constant(""), rooms: []) } } diff --git a/Nio/Conversations/RoomView.swift b/Nio/Conversations/RoomView.swift index bc3a0cab..2b3bfecc 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -31,6 +31,7 @@ struct RoomView: View { @State private var attributedMessage = NSAttributedString(string: "") @State private var shouldPaginate = false + @State private var canScrollFurther = true private var areOtherUsersTyping: Bool { !(room.room.typingUsers?.filter { $0 != userId }.isEmpty ?? true) @@ -38,7 +39,7 @@ struct RoomView: View { var body: some View { VStack { - ReverseList(events.renderableEvents, hasReachedTop: $shouldPaginate) { event in + ReverseList(events.renderableEvents, hasReachedTop: $shouldPaginate, canScrollFurther: $canScrollFurther) { event in EventContainerView(event: event, reactions: self.events.reactions(for: event), connectedEdges: self.events.connectedEdges(of: event), @@ -83,8 +84,7 @@ struct RoomView: View { .onChange(of: shouldPaginate) { newValue in if newValue, let topEvent = events.renderableEvents.first { asyncDetached { - print("paginating") - await room.paginate(topEvent) + await paginate(topEvent: topEvent) } } } @@ -120,6 +120,16 @@ struct RoomView: View { } } + private nonisolated func paginate(topEvent: MXEvent) async { + print("paginating") + let canScroll = await room.paginate(topEvent) + await self.setCanScroll(to: canScroll) + } + + private func setCanScroll(to canScroll: Bool) { + self.canScrollFurther = canScroll + } + private func send() { if editEventId == nil { onCommit(attributedMessage.string) diff --git a/Nio/Shared Views/ReverseList.swift b/Nio/Shared Views/ReverseList.swift index 4547ed63..966b2d24 100644 --- a/Nio/Shared Views/ReverseList.swift +++ b/Nio/Shared Views/ReverseList.swift @@ -14,11 +14,13 @@ struct ReverseList: View where Element: Identifiable, Content: private let viewForItem: (Element) -> Content @Binding var hasReachedTop: Bool + @Binding var canScrollFurther: Bool - init(_ items: [Element], reverseItemOrder: Bool = true, hasReachedTop: Binding, viewForItem: @escaping (Element) -> Content) { + init(_ items: [Element], reverseItemOrder: Bool = true, hasReachedTop: Binding, canScrollFurther: Binding = .constant(true) , viewForItem: @escaping (Element) -> Content) { self.items = items self.reverseItemOrder = reverseItemOrder self._hasReachedTop = hasReachedTop + self._canScrollFurther = canScrollFurther self.viewForItem = viewForItem } @@ -34,12 +36,14 @@ struct ReverseList: View where Element: Identifiable, Content: let frame = topViewGeometry.frame(in: .global) let isVisible = contentsGeometry.frame(in: .global).contains(CGPoint(x: frame.midX, y: frame.midY)) - HStack { - Spacer() - ProgressView().progressViewStyle(CircularProgressViewStyle()) - Spacer() + if canScrollFurther { + HStack { + Spacer() + ProgressView().progressViewStyle(CircularProgressViewStyle()) + Spacer() + } + .preference(key: IsVisibleKey.self, value: isVisible) } - .preference(key: IsVisibleKey.self, value: isVisible) } .frame(height: 30) // FIXME: Frame height shouldn't be hard-coded .onPreferenceChange(IsVisibleKey.self) { diff --git a/NioIntentsExtension/IntentHandler.swift b/NioIntentsExtension/IntentHandler.swift index 5d9afde4..6ba0b083 100644 --- a/NioIntentsExtension/IntentHandler.swift +++ b/NioIntentsExtension/IntentHandler.swift @@ -48,7 +48,7 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo //await Task.sleep(20_000) // TODO: wait at a better place - while await store.loginState.isAuthenticating { + while store.loginState.isAuthenticating { // FIXME: !!!!!!! #warning("this is not good coding!!!!!") await Task.yield() @@ -58,7 +58,7 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo var resolutionResults = [INSendMessageRecipientResolutionResult]() - print("group name: \(intent.speakableGroupName)") + print("group name: \(String(describing: intent.speakableGroupName))") for recipient in recipients { print("handle: \(String(describing: recipient.personHandle))") diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index 2f526e3c..3ad06a9e 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -84,6 +84,7 @@ public class NIORoom: ObservableObject { } } + // MARK: - init public init(_ room: MXRoom) { self.room = room @@ -381,40 +382,29 @@ public class NIORoom: ObservableObject { //private var lastPaginatedEvent: MXEvent? private var timeline: MXEventTimeline? - public func paginate(_ event: MXEvent, direction: MXTimelineDirection = .backwards, numItems: UInt = 40) async { - /*guard event != lastPaginatedEvent else { - return - }*/ - + public func paginate(_ event: MXEvent, direction: MXTimelineDirection = .backwards, numItems: UInt = 40) async -> Bool { if timeline == nil { return await createPagination() - /*Self.logger.debug("creating timeline for room '\(self.displayName)' with event '\(event.eventId)'") - //lastPaginatedEvent = event - timeline = room.timeline(onEvent: event.eventId) - let _ = timeline?.listenToEvents { - event, direction, roomState in - if direction == .backwards { - // eventCache is published, so no objectWillChanges.send here - self.add(event: event, direction: direction, roomState: roomState) - } - } - timeline?.resetPagination()*/ } if timeline?.canPaginate(direction) ?? false { do { try await timeline?.paginate(numItems, direction: direction, onlyFromStore: false) + return true } catch { Self.logger.warning("could not paginate: \(error.localizedDescription)") + return false } } else { Self.logger.debug("cannot paginate: \(self.displayName)") + return false } } - public func createPagination() async { + public func createPagination() async -> Bool { guard timeline == nil else { - return + Self.logger.critical("createPagination called while a timeline already exists. This could be a critical bug") + return false } Self.logger.debug("Bootstraping pagination") @@ -423,11 +413,14 @@ public class NIORoom: ObservableObject { if timeline?.canPaginate(.backwards) ?? false { do { try await timeline?.paginate(40, direction: .backwards) + return true } catch { Self.logger.warning("could not bootstrap pagination: \(error.localizedDescription)") + return false } } else { Self.logger.warning("could not bootstrap pagination") + return false } } From f1c8c420e68da498d820bc98f14128ff40c3ed51 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 25 Jun 2021 14:05:03 +0200 Subject: [PATCH 09/11] create async RequestSiriAuthoriziation --- Nio/AppDelegate.swift | 5 ++++- Nio/NioApp.swift | 9 --------- NioKit/Extensions/INPreferences+async.swift | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 NioKit/Extensions/INPreferences+async.swift diff --git a/Nio/AppDelegate.swift b/Nio/AppDelegate.swift index 393fd559..bf583859 100644 --- a/Nio/AppDelegate.swift +++ b/Nio/AppDelegate.swift @@ -33,8 +33,9 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - let notificationCenter = UNUserNotificationCenter.current() + let notificationCenter = UNUserNotificationCenter.current() + self.createMessageActions(notificationCenter: notificationCenter) async { @@ -45,6 +46,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { } catch { print("error requesting UNUserNotificationCenter: \(error.localizedDescription)") } + + await INPreferences.requestSiriAuthorization() } print("Your code here") diff --git a/Nio/NioApp.swift b/Nio/NioApp.swift index 4949cf47..5e33edc9 100644 --- a/Nio/NioApp.swift +++ b/Nio/NioApp.swift @@ -38,15 +38,6 @@ struct NioApp: App { print("found string \(id)") }*/ }) - .onAppear { - async { - let _: INSiriAuthorizationStatus = await withCheckedContinuation {continuation in - INPreferences.requestSiriAuthorization({status in - continuation.resume(returning: status) - }) - } - } - } #endif } diff --git a/NioKit/Extensions/INPreferences+async.swift b/NioKit/Extensions/INPreferences+async.swift new file mode 100644 index 00000000..7969afeb --- /dev/null +++ b/NioKit/Extensions/INPreferences+async.swift @@ -0,0 +1,19 @@ +// +// INPreferences+async.swift +// Nio +// +// Created by Finn Behrens on 25.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Foundation +import Intents + +extension INPreferences { + @discardableResult + public static func requestSiriAuthorization() async -> INSiriAuthorizationStatus { + return await withCheckedContinuation { continuation in + INPreferences.requestSiriAuthorization({ continuation.resume(returning: $0) }) + } + } +} From 93cea34af5a0cd4c78edb0b56d4ad4748e39a172 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 25 Jun 2021 14:39:31 +0200 Subject: [PATCH 10/11] drag keyboard away --- .gitignore | 1 + Nio.xcodeproj/project.pbxproj | 6 ++++++ Nio/Conversations/RoomView.swift | 8 ++++++++ {NioKit => Nio}/Extensions/INPreferences+async.swift | 0 4 files changed, 15 insertions(+) rename {NioKit => Nio}/Extensions/INPreferences+async.swift (100%) diff --git a/.gitignore b/.gitignore index 222fc7a1..dd259110 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Pods/CocoaPodsKeys fastlane/Appfile Configs/LocalConfig.xcconfig +.DS_Store diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 417d965a..123bdf55 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -86,6 +86,8 @@ 9525D2EC267659A2004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */; }; 9525D2ED267659BD004055B6 /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D2E62676594F004055B6 /* SettingsContainerView.swift */; }; 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; + 95860113268604EE00C4065F /* INPreferences+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95860112268604EE00C4065F /* INPreferences+async.swift */; }; + 95860114268604EE00C4065F /* INPreferences+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95860112268604EE00C4065F /* INPreferences+async.swift */; }; 95A11CD2267CA8810094AC5F /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A11CD1267CA8810094AC5F /* NotificationService.swift */; }; 95A11CD6267CA8810094AC5F /* NioNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 95A11CCF267CA8810094AC5F /* NioNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; @@ -448,6 +450,7 @@ 9525D2E326765850004055B6 /* RoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomView.swift; sourceTree = ""; }; 9525D2E62676594F004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; + 95860112268604EE00C4065F /* INPreferences+async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "INPreferences+async.swift"; sourceTree = ""; }; 95A11CCF267CA8810094AC5F /* NioNSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NioNSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 95A11CD1267CA8810094AC5F /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 95A11CD3267CA8810094AC5F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -851,6 +854,7 @@ 39D166C42385C7F6006DD257 /* Extensions */ = { isa = PBXGroup; children = ( + 95860112268604EE00C4065F /* INPreferences+async.swift */, CAC46D5A23A2734C0079C24F /* EnvironmentValues.swift */, CAFCB322245F6E6700869320 /* NSAttributedString+Extensions.swift */, 39D166C52385C804006DD257 /* String+Emoji.swift */, @@ -1627,6 +1631,7 @@ 392221B4243D1627004D8794 /* RoomPowerLevelsEventView.swift in Sources */, CAF2AE92245AEEBA00D84133 /* BadgeView.swift in Sources */, CAC46D6323A278F40079C24F /* PreviewProvider+Enumeration.swift in Sources */, + 95860113268604EE00C4065F /* INPreferences+async.swift in Sources */, 9525D2D82676548B004055B6 /* RecentRoomsView.swift in Sources */, 39D166C82385C832006DD257 /* EventContainerView.swift in Sources */, CAC46D6B23A55E940079C24F /* PeekableIterator.swift in Sources */, @@ -1776,6 +1781,7 @@ E843F30725F2781800B0F33B /* GroupingIterator.swift in Sources */, 9525D2E026765754004055B6 /* ImagePicker.swift in Sources */, E843F31825F2782B00B0F33B /* BorderedMessageView.swift in Sources */, + 95860114268604EE00C4065F /* INPreferences+async.swift in Sources */, E843F32B25F2783900B0F33B /* ContentSizeCategory.swift in Sources */, E843F31025F2782000B0F33B /* EventContextMenu.swift in Sources */, E843F31D25F2782E00B0F33B /* GenericEventView.swift in Sources */, diff --git a/Nio/Conversations/RoomView.swift b/Nio/Conversations/RoomView.swift index 2b3bfecc..d51de153 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -78,6 +78,14 @@ struct RoomView: View { onCancel: cancelEdit, onCommit: send ) + .gesture(DragGesture(minimumDistance: 0.3, coordinateSpace: .local) + .onEnded({ value in + if value.translation.height > 0 { + isEditingMessage = false + } + }) + ) + .padding(.horizontal) .padding(.bottom, 10) } diff --git a/NioKit/Extensions/INPreferences+async.swift b/Nio/Extensions/INPreferences+async.swift similarity index 100% rename from NioKit/Extensions/INPreferences+async.swift rename to Nio/Extensions/INPreferences+async.swift From c8cb09ab773a95f419b3e494243cc2da48f2cae9 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 30 Jul 2021 11:41:41 +0200 Subject: [PATCH 11/11] rename NSUserActivity --- Nio.xcodeproj/project.pbxproj | 6 ++ .../Event Views/GenericEventView.swift | 17 ++-- .../MessageView/MediaEventView.swift | 85 +++++++++++++------ .../RecentRoomsContainerView.swift | 1 - Nio/Conversations/RoomListItemView.swift | 10 +-- Nio/Conversations/RoomView.swift | 2 +- Nio/Info.plist | 2 +- Nio/NioApp.swift | 2 +- NioIntentsExtension/IntentHandler.swift | 4 +- NioKit/Extensions/MXMediaManager+Async.swift | 26 ++++++ NioKit/Models/NIORoom.swift | 4 +- NioKit/Session/AccountStore.swift | 48 +++++++---- NioNSE/NotificationService.swift | 2 +- 13 files changed, 147 insertions(+), 62 deletions(-) create mode 100644 NioKit/Extensions/MXMediaManager+Async.swift diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 123bdf55..4b11ea76 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -88,6 +88,8 @@ 9525D2EE26765C8C004055B6 /* RecentRoomsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C932062384BB13004449E1 /* RecentRoomsContainerView.swift */; }; 95860113268604EE00C4065F /* INPreferences+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95860112268604EE00C4065F /* INPreferences+async.swift */; }; 95860114268604EE00C4065F /* INPreferences+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95860112268604EE00C4065F /* INPreferences+async.swift */; }; + 9586011826861BA400C4065F /* MXMediaManager+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95860115268619C000C4065F /* MXMediaManager+Async.swift */; }; + 9586011926861BA500C4065F /* MXMediaManager+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95860115268619C000C4065F /* MXMediaManager+Async.swift */; }; 95A11CD2267CA8810094AC5F /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A11CD1267CA8810094AC5F /* NotificationService.swift */; }; 95A11CD6267CA8810094AC5F /* NioNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 95A11CCF267CA8810094AC5F /* NioNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 95CAC7B82678F64E001D71DC /* MXEventTimeLine+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */; }; @@ -451,6 +453,7 @@ 9525D2E62676594F004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 9525D2EB267659A2004055B6 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; 95860112268604EE00C4065F /* INPreferences+async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "INPreferences+async.swift"; sourceTree = ""; }; + 95860115268619C000C4065F /* MXMediaManager+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXMediaManager+Async.swift"; sourceTree = ""; }; 95A11CCF267CA8810094AC5F /* NioNSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NioNSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 95A11CD1267CA8810094AC5F /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 95A11CD3267CA8810094AC5F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1028,6 +1031,7 @@ E897AA3325F2716F00D11427 /* UXKit.swift */, 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */, 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */, + 95860115268619C000C4065F /* MXMediaManager+Async.swift */, ); path = Extensions; sourceTree = ""; @@ -1710,6 +1714,7 @@ E8B4725B25F26D5A00ACEFCB /* Reaction.swift in Sources */, 9525D2CF26763DCB004055B6 /* MXSession+Async.swift in Sources */, 95CAC88B26791913001D71DC /* MXRoomMember+INPerson.swift in Sources */, + 9586011926861BA500C4065F /* MXMediaManager+Async.swift in Sources */, E8B4725525F26D5A00ACEFCB /* MXClient+Publisher.swift in Sources */, E897AA3425F2716F00D11427 /* UXKit.swift in Sources */, 9525D2D626763DD0004055B6 /* MXRoom+Async.swift in Sources */, @@ -1798,6 +1803,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9586011826861BA400C4065F /* MXMediaManager+Async.swift in Sources */, 9525D2D126763DD0004055B6 /* MXRestClient+Async.swift in Sources */, E897AA2025F2707C00D11427 /* NIORoomSummary.swift in Sources */, E897AA2525F2707C00D11427 /* ReactionEvent.swift in Sources */, diff --git a/Nio/Conversations/Event Views/GenericEventView.swift b/Nio/Conversations/Event Views/GenericEventView.swift index 35b9bd21..02fe27ff 100644 --- a/Nio/Conversations/Event Views/GenericEventView.swift +++ b/Nio/Conversations/Event Views/GenericEventView.swift @@ -1,5 +1,4 @@ import SwiftUI -import SDWebImageSwiftUI import NioKit @@ -18,11 +17,17 @@ struct GenericEventView: View { HStack(spacing: 4) { Spacer() if imageURL != nil { - WebImage(url: imageURL!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 15, height: 15) - .mask(Circle()) + AsyncImage(url: imageURL, content: {image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 15, height: 15) + .mask(Circle()) + }, placeholder: { + ProgressView() + .frame(width: 15, height: 15) + .mask(Circle()) + }) } Text(text) .font(.caption) diff --git a/Nio/Conversations/Event Views/MessageView/MediaEventView.swift b/Nio/Conversations/Event Views/MessageView/MediaEventView.swift index e56da1cf..f3b7639d 100644 --- a/Nio/Conversations/Event Views/MessageView/MediaEventView.swift +++ b/Nio/Conversations/Event Views/MessageView/MediaEventView.swift @@ -1,5 +1,6 @@ import SwiftUI import class MatrixSDK.MXEvent +import class NioKit.AccountStore import SDWebImageSwiftUI #if os(macOS) @@ -12,12 +13,15 @@ struct MediaEventView: View { @Environment(\.homeserver) private var homeserver struct ViewModel { + fileprivate let event: MXEvent? fileprivate let mediaURLs: [MXURL] fileprivate let sender: String fileprivate let showSender: Bool fileprivate let timestamp: String fileprivate var size: CGSize? fileprivate var blurhash: String? + + @State private var imageUrl: URL? init(mediaURLs: [String], sender: String, @@ -25,6 +29,7 @@ struct MediaEventView: View { timestamp: String, size: CGSize?, blurhash: String?) { + self.event = nil self.mediaURLs = mediaURLs.compactMap(MXURL.init) self.sender = sender self.showSender = showSender @@ -34,6 +39,7 @@ struct MediaEventView: View { } init(event: MXEvent, showSender: Bool) { + self.event = event self.mediaURLs = event .getMediaURLs() .compactMap(MXURL.init) @@ -49,12 +55,14 @@ struct MediaEventView: View { self.blurhash = info["xyz.amorgan.blurhash"] as? String } } + } let model: ViewModel let contextMenuModel: EventContextMenuModel @ViewBuilder + @MainActor var placeholder: some View { // TBD: isn't there a "placeholder" generator in SwiftUI now? #if os(macOS) @@ -73,10 +81,18 @@ struct MediaEventView: View { } var urls: [URL] { - model.mediaURLs.compactMap { mediaURL in + return model.mediaURLs.compactMap { mediaURL in mediaURL.contentURL(on: self.homeserver) } } + @State private var encryptedUrl: String? + var encrpytedUiImage: UIImage? { + guard let encryptedUrl = encryptedUrl else { + return nil + } + print("trying to load image: \(encryptedUrl)") + return UIImage(contentsOfFile: encryptedUrl) + } private var isMe: Bool { model.sender == userId @@ -95,37 +111,56 @@ struct MediaEventView: View { } var body: some View { - #if os(macOS) VStack(alignment: isMe ? .trailing : .leading, spacing: 5) { senderView - WebImage(url: urls.first, isAnimating: .constant(true)) - .resizable() - .placeholder { placeholder } - .indicator(.activity) - .aspectRatio(model.size ?? CGSize(width: 3, height: 2), contentMode: .fit) - .mask(RoundedRectangle(cornerRadius: 15)) - timestampView - } - .contextMenu(ContextMenu(menuItems: { - EventContextMenu(model: contextMenuModel) - })) - #else - VStack(alignment: isMe ? .trailing : .leading, spacing: 5) { - senderView - WebImage(url: urls.first, isAnimating: .constant(true)) - .resizable() - .placeholder { placeholder } - .indicator(.activity) - .aspectRatio(model.size ?? CGSize(width: 3, height: 2), contentMode: .fit) - .mask(RoundedRectangle(cornerRadius: 15)) + if let encrpytedUiImage = encrpytedUiImage { + Image(uiImage: encrpytedUiImage) + } else { + WebImage(url: urls.first, isAnimating: .constant(true)) + .resizable() + .placeholder { placeholder } + .indicator(.activity) + .aspectRatio(model.size ?? CGSize(width: 3, height: 2), contentMode: .fit) + .mask(RoundedRectangle(cornerRadius: 15)) + // TODO: use AsyncImage (currently not supporting gifs) + /*AsyncImage(url: self.urls.first, content: {phase in + switch phase { + case .empty: + placeholder + case .success(let image): + image + .resizable() + .aspectRatio(model.size ?? CGSize(width: 3, height: 2), contentMode: .fit) + .mask(RoundedRectangle(cornerRadius: 15)) + case .failure(let error): + Text("Error loading picture \(error.localizedDescription)") + + default: + placeholder + .onAppear(perform: { + print("This case to AsyncImage is unknown (new)") + }) + } + })*/ + .accessibility(label: Text("Image \(urls.first?.absoluteString ?? "")")) + } timestampView } - .frame(maxWidth: UIScreen.main.bounds.width * 0.75, - maxHeight: UIScreen.main.bounds.height * 0.75) .contextMenu(ContextMenu(menuItems: { EventContextMenu(model: contextMenuModel) })) - #endif + #if os(iOS) + .frame(maxWidth: UIScreen.main.bounds.width * 0.75, + maxHeight: UIScreen.main.bounds.height * 0.75) + #endif + .task { + guard let event = self.model.event else { + return + } + if event.isEncrypted { + self.encryptedUrl = await AccountStore.shared.downloadEncrpytedMedia(event: event) + } + } } } diff --git a/Nio/Conversations/RecentRoomsContainerView.swift b/Nio/Conversations/RecentRoomsContainerView.swift index c9c937b4..11ba53fb 100644 --- a/Nio/Conversations/RecentRoomsContainerView.swift +++ b/Nio/Conversations/RecentRoomsContainerView.swift @@ -82,7 +82,6 @@ struct RoomsListSection: View { NavigationLink(destination: RoomContainerView(room: room), tag: room.id, selection: $selectedRoomId) { RoomListItemContainerView(room: room) } - //.searchCompletion(room.displayName) } .onDelete(perform: setLeaveIndex) } diff --git a/Nio/Conversations/RoomListItemView.swift b/Nio/Conversations/RoomListItemView.swift index a36b4f09..f13de38d 100644 --- a/Nio/Conversations/RoomListItemView.swift +++ b/Nio/Conversations/RoomListItemView.swift @@ -1,6 +1,5 @@ import SwiftUI import MatrixSDK -import SDWebImageSwiftUI import NioKit @@ -149,14 +148,11 @@ struct RoomListItemView: View { } @ViewBuilder private var image: some View { - if let avatarURL = roomAvatarURL { - WebImage(url: avatarURL) + AsyncImage(url: roomAvatarURL, content: { image in + image .resizable() - .placeholder { prefixAvatar } .aspectRatio(contentMode: .fill) - } else { - prefixAvatar - } + }, placeholder: { prefixAvatar }) } @Environment(\.sizeCategory) private var sizeCategory diff --git a/Nio/Conversations/RoomView.swift b/Nio/Conversations/RoomView.swift index d51de153..e3a936df 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -107,7 +107,7 @@ struct RoomView: View { primaryButton: .destructive(Text(verbatim: L10n.Room.Remove.action), action: { self.onRedact(eventId, nil) }), secondaryButton: .cancel()) } - .userActivity("chat.nio.chat") { userActivity in + .userActivity("org.matrix.room") { userActivity in userActivity.isEligibleForHandoff = true userActivity.isEligibleForSearch = true userActivity.isEligibleForPrediction = true diff --git a/Nio/Info.plist b/Nio/Info.plist index b6370487..f6a2595b 100644 --- a/Nio/Info.plist +++ b/Nio/Info.plist @@ -73,7 +73,7 @@ NSUserActivityTypes INSendMessageIntent - chat.nio.chat + org.matrix.room UIApplicationSceneManifest diff --git a/Nio/NioApp.swift b/Nio/NioApp.swift index 5e33edc9..164827d3 100644 --- a/Nio/NioApp.swift +++ b/Nio/NioApp.swift @@ -28,7 +28,7 @@ struct NioApp: App { RootView() .environmentObject(accountStore) .accentColor(accentColor) - .onContinueUserActivity("chat.nio.chat", perform: {activity in + .onContinueUserActivity("org.matrix.room", perform: {activity in print("handling activity: \(activity)") if let id = activity.userInfo?["id"] as? String { print("restored room: \(id)") diff --git a/NioIntentsExtension/IntentHandler.swift b/NioIntentsExtension/IntentHandler.swift index 6ba0b083..11ae9d95 100644 --- a/NioIntentsExtension/IntentHandler.swift +++ b/NioIntentsExtension/IntentHandler.swift @@ -144,12 +144,12 @@ public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo public func handle(intent: INSendMessageIntent) async -> INSendMessageIntentResponse { print("handle INSendMessageIntent") // Implement your application logic to send a message here. - let store = AccountStore.shared + let store = await AccountStore.shared guard let recipient = intent.recipients?.first?.customIdentifier else { return INSendMessageIntentResponse(code: .failure, userActivity: nil) } - let userActivity = NSUserActivity(activityType: "chat.nio.chat") + let userActivity = NSUserActivity(activityType: "org.matrix.room") userActivity.isEligibleForSearch = true userActivity.isEligibleForHandoff = true userActivity.isEligibleForPrediction = true diff --git a/NioKit/Extensions/MXMediaManager+Async.swift b/NioKit/Extensions/MXMediaManager+Async.swift new file mode 100644 index 00000000..aec2ec20 --- /dev/null +++ b/NioKit/Extensions/MXMediaManager+Async.swift @@ -0,0 +1,26 @@ +// +// MXMediaManager+Async.swift +// Nio +// +// Created by Finn Behrens on 25.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Foundation +import MatrixSDK + +extension MXMediaManager { + + /** + Download encrypted data from the Matrix Content repository. + + @param encryptedContentFile the encrypted Matrix Content details. + @param folder the cache folder to use (may be nil). kMXMediaManagerDefaultCacheFolder is used by default. + @return the path of the resulting file. + */ + public func downloadEncryptedMedia(fromMatrixContentFile contentFile: MXEncryptedContentFile, inFolder folder: String?) async throws -> String { + return try await withCheckedThrowingContinuation {continuation in + self.downloadEncryptedMedia(fromMatrixContentFile: contentFile, inFolder: folder, success: {value in continuation.resume(returning: value!)}, failure: {e in continuation.resume(throwing: e!)}) + } + } +} diff --git a/NioKit/Models/NIORoom.swift b/NioKit/Models/NIORoom.swift index 3ad06a9e..8a27d4eb 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -29,7 +29,7 @@ public struct RoomItem: Codable, Hashable { public class NIORoom: ObservableObject { static let logger = Logger(subsystem: "chat.nio", category: "ROOM") - public let room: MXRoom + public nonisolated let room: MXRoom @Published public var summary: NIORoomSummary @@ -166,7 +166,7 @@ public class NIORoom: ObservableObject { } let isMe = event.sender == selfId - let userActivity = NSUserActivity(activityType: "chat.nio.chat") + let userActivity = NSUserActivity(activityType: "org.matrix.room") userActivity.isEligibleForHandoff = true userActivity.isEligibleForSearch = true userActivity.isEligibleForPrediction = true diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 998472e0..3cc6d4a5 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -32,7 +32,7 @@ public enum LoginState { public class AccountStore: ObservableObject { static let logger = Logger(subsystem: "chat.nio.chat", category: "AccountStore") - public static let shared = AccountStore() + public static nonisolated let shared = AccountStore() public var client: MXRestClient? public var session: MXSession? @@ -45,7 +45,7 @@ public class AccountStore: ObservableObject { accessGroup: ((Bundle.main.infoDictionary?["DevelopmentTeam"] as? String) ?? "") + ".nio.keychain" ) - public init() { + public nonisolated init() { if CommandLine.arguments.contains("-clear-stored-credentials") { print("🗑 cleared stored credentials from keychain") MXCredentials @@ -66,21 +66,25 @@ public class AccountStore: ObservableObject { return } self.credentials = credentials - loginState = .authenticating async { - do { - self.loginState = try await self.sync() - self.session?.crypto.warnOnUnknowDevices = false - } catch { - print("Error on starting session with saved credentials: \(error)") - self.loginState = .failure(error) - } + await self.init_sync() } } deinit { self.session?.removeListener(self.listenReference) } + + private func init_sync() async { + loginState = .authenticating + do { + self.loginState = try await self.sync() + self.session?.crypto.warnOnUnknowDevices = false + } catch { + print("Error on starting session with saved credentials: \(error)") + self.loginState = .failure(error) + } + } // MARK: - Login & Sync @@ -273,12 +277,26 @@ public class AccountStore: ObservableObject { Self.logger.debug("got pushers: \(String(describing: pushers))") } try await session.matrixRestClient.setPusher(puskKey: pushKey, kind: enable ? .http : .none, appId: appId, appDisplayName: "Nio", deviceDisplayName: "NioiOS", profileTag: "gloaaabal", lang: lang, data: data, append: false) - //session.matrixRestClient.setPusher(pushKey: key, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) - //self.session?.matrixRestClient.setPusher(pushKey: <#T##String#>, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) } - /*func setPusher() { - self.session?.matrixRestClient.setPusher(pushKey: <#T##String#>, kind: .http, appId: <#T##String#>, appDisplayName: <#T##String#>, deviceDisplayName: <#T##String#>, profileTag: <#T##String#>, lang: <#T##String#>, data: <#T##[String : Any]#>, append: <#T##Bool#>, completion: <#T##(MXResponse) -> Void#>) - }*/ + + public func downloadEncrpytedMedia(event: MXEvent) async -> String? { + guard let session = session else { + return nil + } + + guard let file = event.getEncryptedContentFiles().first else { + return nil + } + + do { + let filePath = try await session.mediaManager.downloadEncryptedMedia(fromMatrixContentFile: file, inFolder: nil) + Self.logger.debug("got encrypted media file: \(filePath)") + return filePath + } catch { + Self.logger.info("Could not download encrpyted media: \(error.localizedDescription)") + return nil + } + } } enum AccountStoreError: Error { diff --git a/NioNSE/NotificationService.swift b/NioNSE/NotificationService.swift index 25abf5f4..105c1327 100644 --- a/NioNSE/NotificationService.swift +++ b/NioNSE/NotificationService.swift @@ -26,7 +26,7 @@ class NotificationService: UNNotificationServiceExtension { print("didReceive") print(bestAttemptContent?.userInfo as Any) if let bestAttemptContent = bestAttemptContent { - let store = AccountStore.shared + let store = await AccountStore.shared // Modify the notification content here... //bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"