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/Configs/GlobalConfig.xcconfig b/Configs/GlobalConfig.xcconfig index 83152481..67a71eed 100644 --- a/Configs/GlobalConfig.xcconfig +++ b/Configs/GlobalConfig.xcconfig @@ -8,13 +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 -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/Mio/Conversations/RecentRoomsView.swift b/Mio/Conversations/RecentRoomsView.swift new file mode 100644 index 00000000..3cbde163 --- /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: MXRoom.MXRoomId? + + 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..04784822 --- /dev/null +++ b/Mio/Conversations/RoomContainerView.swift @@ -0,0 +1,86 @@ +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 + asyncDetached { + await 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 e5ed49eb..4b11ea76 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 */; }; @@ -62,6 +61,53 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; @@ -103,9 +149,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 */; }; @@ -114,8 +158,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 */; }; @@ -139,7 +181,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 */; }; @@ -205,6 +246,41 @@ 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 */; + proxyType = 1; + remoteGlobalIDString = 95CAC87126791295001D71DC; + remoteInfo = NioIntentsExtensionUI; + }; + 95CAC88026791295001D71DC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39C931D123843289004449E1 /* Project object */; + proxyType = 1; + 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 */; @@ -242,7 +318,10 @@ 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 */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -272,7 +351,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 = ""; }; @@ -334,8 +412,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 = ""; }; @@ -360,6 +438,43 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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; }; + 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 = ""; }; + 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 = ""; }; @@ -428,6 +543,33 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95A11CCC267CA8810094AC5F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 95036C96267DF7A300E1EDC0 /* libNioKit-iOS.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 95CAC86426791295001D71DC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC86926791295001D71DC /* Intents.framework in Frameworks */, + 95EF1D7E267F7C7C000FAEF0 /* CommonMarkAttributedString in Frameworks */, + 95EF1D7C267F7B98000FAEF0 /* MatrixSDK in Frameworks */, + 95EF1D78267F7B94000FAEF0 /* libNioKit-iOS.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 95CAC86F26791295001D71DC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 95CAC87426791295001D71DC /* IntentsUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CADF663224614D2300F5063F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -471,8 +613,8 @@ 3902B8A12395934900698B87 /* Settings */ = { isa = PBXGroup; children = ( - 3902B8A22395935600698B87 /* SettingsView.swift */, 39DD77AE247006E300A29DEE /* AppIcon.swift */, + 9525D2E62676594F004055B6 /* SettingsContainerView.swift */, ); path = Settings; sourceTree = ""; @@ -552,6 +694,7 @@ 39222198243689D6004D8794 /* ReactionEvent.swift */, 4B058B5524573A570059BC75 /* EditEvent.swift */, 3922219C24368B25004D8794 /* CustomEvent.swift */, + 95036C9E267E83D000E1EDC0 /* ReplyEvent.swift */, ); path = "Custom Events"; sourceTree = ""; @@ -625,6 +768,9 @@ CADF663B24614D2300F5063F /* NioKitTests */, 4BFEFD7D246F414D00CCF4A0 /* NioShareExtension */, E843F2DB25F2748200B0F33B /* Mio */, + 95CAC86A26791295001D71DC /* NioIntentsExtension */, + 95CAC87526791295001D71DC /* NioIntentsExtensionUI */, + 95A11CD0267CA8810094AC5F /* NioNSE */, 39C931DA2384328A004449E1 /* Products */, A58352AA25A667B300533363 /* Frameworks */, ); @@ -641,6 +787,9 @@ E8302D4C25F26CA500E962E9 /* libNioKit-iOS.a */, E897AA1525F2706D00D11427 /* libNioKit-macOS.a */, E843F2DA25F2748200B0F33B /* Mio.app */, + 95CAC86726791295001D71DC /* NioIntentsExtension.appex */, + 95CAC87226791295001D71DC /* NioIntentsExtensionUI.appex */, + 95A11CCF267CA8810094AC5F /* NioNSE.appex */, ); name = Products; sourceTree = ""; @@ -651,6 +800,7 @@ A51BF8CD254C2FE5000FB0A4 /* NioApp.swift */, 4BFEFD90246F5EF500CCF4A0 /* Nio.entitlements */, 39C931E02384328A004449E1 /* RootView.swift */, + 95CAC88D26791D11001D71DC /* AppDelegate.swift */, 3902B8A62395A78100698B87 /* Authentication */, CAC46D5E23A276B30079C24F /* Shapes */, 392389CA238EBB0800B2E1DF /* Shared Views */, @@ -691,13 +841,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 = ""; @@ -705,6 +857,7 @@ 39D166C42385C7F6006DD257 /* Extensions */ = { isa = PBXGroup; children = ( + 95860112268604EE00C4065F /* INPreferences+async.swift */, CAC46D5A23A2734C0079C24F /* EnvironmentValues.swift */, CAFCB322245F6E6700869320 /* NSAttributedString+Extensions.swift */, 39D166C52385C804006DD257 /* String+Emoji.swift */, @@ -772,9 +925,72 @@ 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 = ""; + }; + 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 = ( + 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 */, + 95DE061F267B4D5800832844 /* UserNotifications.framework */, + 95DE0621267B4D5800832844 /* UserNotificationsUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -803,12 +1019,19 @@ 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 */, 393411C823904428003B49B8 /* MXEvent+Extensions.swift */, 4BEB8C03250403D200E90699 /* UserDefaults.swift */, E897AA3325F2716F00D11427 /* UXKit.swift */, + 95CAC7B72678F64E001D71DC /* MXEventTimeLine+Async.swift */, + 95CAC88A26791913001D71DC /* MXRoomMember+INPerson.swift */, + 95860115268619C000C4065F /* MXMediaManager+Async.swift */, ); path = Extensions; sourceTree = ""; @@ -858,6 +1081,9 @@ E843F2DB25F2748200B0F33B /* Mio */ = { isa = PBXGroup; children = ( + 9525D2EA26765994004055B6 /* Settings */, + 9525D2DC267656EE004055B6 /* Shared Views */, + 9525D2D9267654D3004055B6 /* Conversations */, E843F2E025F2748300B0F33B /* Assets.xcassets */, E843F2E525F2748300B0F33B /* Info.plist */, E843F2E625F2748300B0F33B /* Mio.entitlements */, @@ -925,6 +1151,9 @@ dependencies = ( E8B4726925F26DCB00ACEFCB /* PBXTargetDependency */, 4BFEFD85246F414D00CCF4A0 /* PBXTargetDependency */, + 95CAC87D26791295001D71DC /* PBXTargetDependency */, + 95CAC88126791295001D71DC /* PBXTargetDependency */, + 95A11CD5267CA8810094AC5F /* PBXTargetDependency */, ); name = Nio; packageProductDependencies = ( @@ -956,6 +1185,65 @@ 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" */; + buildPhases = ( + 95CAC86326791295001D71DC /* Sources */, + 95CAC86426791295001D71DC /* Frameworks */, + 95CAC86526791295001D71DC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 95EF1D7A267F7B94000FAEF0 /* PBXTargetDependency */, + ); + name = NioIntentsExtension; + packageProductDependencies = ( + 95EF1D7B267F7B98000FAEF0 /* MatrixSDK */, + 95EF1D7D267F7C7C000FAEF0 /* CommonMarkAttributedString */, + ); + 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; + packageProductDependencies = ( + ); + 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" */; @@ -1050,7 +1338,7 @@ 39C931D123843289004449E1 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1250; + LastSwiftUpdateCheck = 1300; LastUpgradeCheck = 1250; ORGANIZATIONNAME = "Kilian Koeltzsch"; TargetAttributes = { @@ -1064,6 +1352,15 @@ 4BFEFD7B246F414D00CCF4A0 = { CreatedOnToolsVersion = 11.4.1; }; + 95A11CCE267CA8810094AC5F = { + CreatedOnToolsVersion = 13.0; + }; + 95CAC86626791295001D71DC = { + CreatedOnToolsVersion = 13.0; + }; + 95CAC87126791295001D71DC = { + CreatedOnToolsVersion = 13.0; + }; CADF663424614D2300F5063F = { CreatedOnToolsVersion = 11.4.1; LastSwiftMigration = 1140; @@ -1124,6 +1421,9 @@ E897AA1425F2706D00D11427 /* NioKit-macOS */, CADF663424614D2300F5063F /* NioKitTests */, 4BFEFD7B246F414D00CCF4A0 /* NioShareExtension */, + 95CAC86626791295001D71DC /* NioIntentsExtension */, + 95CAC87126791295001D71DC /* NioIntentsExtensionUI */, + 95A11CCE267CA8810094AC5F /* NioNSE */, ); }; /* End PBXProject section */ @@ -1163,6 +1463,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 95A11CCD267CA8810094AC5F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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; @@ -1279,6 +1601,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 */, @@ -1289,11 +1612,12 @@ 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 */, 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 */, @@ -1303,7 +1627,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 */, @@ -1311,11 +1635,13 @@ 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 */, 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 */, @@ -1325,6 +1651,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 */, @@ -1342,6 +1669,33 @@ ); 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; + files = ( + 95CAC89026792247001D71DC /* Intents.intentdefinition in Sources */, + 95CAC86C26791295001D71DC /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 95CAC86E26791295001D71DC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 95036C9C267E1BAA00E1EDC0 /* Intents.intentdefinition in Sources */, + 95CAC87726791295001D71DC /* IntentViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CADF663124614D2300F5063F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1354,17 +1708,26 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; 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 */, + 9586011926861BA500C4065F /* MXMediaManager+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 */, 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 */, E8B4725E25F26D5A00ACEFCB /* MXEvent+Extensions.swift in Sources */, E8B4725425F26D5A00ACEFCB /* MXCredentials+Keychain.swift in Sources */, E8B4726025F26D5A00ACEFCB /* UserDefaults.swift in Sources */, @@ -1376,8 +1739,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 */, @@ -1386,12 +1749,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 */, @@ -1401,8 +1767,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 */, @@ -1411,23 +1775,25 @@ 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 */, + 95860114268604EE00C4065F /* INPreferences+async.swift in Sources */, E843F32B25F2783900B0F33B /* ContentSizeCategory.swift in Sources */, E843F31025F2782000B0F33B /* EventContextMenu.swift in Sources */, E843F31D25F2782E00B0F33B /* GenericEventView.swift in Sources */, 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 */, ); @@ -1437,21 +1803,29 @@ 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 */, + 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 */, E897AA1E25F2707C00D11427 /* MXClient+Publisher.swift in Sources */, E897AA1B25F2707C00D11427 /* Configuration.swift in Sources */, E897AA1C25F2707C00D11427 /* CustomEvent.swift in Sources */, 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 */, E897AA2125F2707C00D11427 /* EventCollection.swift in Sources */, E897AA2625F2707C00D11427 /* UserDefaults.swift in Sources */, E897AA1A25F2707C00D11427 /* NIORoom.swift in Sources */, + 95036CA0267E83D000E1EDC0 /* ReplyEvent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1468,6 +1842,31 @@ 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 */; + targetProxy = 95CAC87C26791295001D71DC /* PBXContainerItemProxy */; + }; + 95CAC88126791295001D71DC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 95CAC86626791295001D71DC /* NioIntentsExtension */; + targetProxy = 95CAC88026791295001D71DC /* PBXContainerItemProxy */; + }; + 95EF1D7A267F7B94000FAEF0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E8302D4B25F26CA500E962E9 /* NioKit-iOS */; + targetProxy = 95EF1D79267F7B94000FAEF0 /* PBXContainerItemProxy */; + }; CADF663A24614D2300F5063F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 39C931D823843289004449E1 /* Nio */; @@ -1539,6 +1938,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 95CAC87826791295001D71DC /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 95CAC87926791295001D71DC /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1712,6 +2119,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 +2144,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 +2165,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 +2187,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", @@ -1793,6 +2202,186 @@ }; 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 = { + 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 = 33; + 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 = 33; + 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_CFBundleExecutable = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleName = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleVersion = 33; + 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_CFBundleExecutable = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleName = NioIntentsExtensionUI; + INFOPLIST_KEY_CFBundleVersion = 33; + 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; + }; CADF664924614D2300F5063F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1843,7 +2432,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 +2447,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 +2473,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 +2497,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 +2511,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 +2526,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; @@ -1983,6 +2574,33 @@ 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 = ( + 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 = ( @@ -2083,6 +2701,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/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.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..bf583859 --- /dev/null +++ b/Nio/AppDelegate.swift @@ -0,0 +1,197 @@ +// +// 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 +import NioKit + +@MainActor +class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject { + public static let shared = AppDelegate() + + @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 { + + let notificationCenter = UNUserNotificationCenter.current() + + self.createMessageActions(notificationCenter: notificationCenter) + + async { + do { + let state = try await notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) + Self.shared.isPushAllowed = state + application.registerForRemoteNotifications() + } catch { + print("error requesting UNUserNotificationCenter: \(error.localizedDescription)") + } + + await INPreferences.requestSiriAuthorization() + } + + print("Your code here") + return true + } + + func application(_ application: UIApplication, + handlerFor intent: INIntent) -> Any? { + print("intent") + print(intent) + //return IntentHandler() + 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) + 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 + } + + 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)") + } +} + + + + // Conform to UNUserNotificationCenterDelegate +extension AppDelegate: UNUserNotificationCenterDelegate { + + /*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]) + + 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/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/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 new file mode 100644 index 00000000..11ba53fb --- /dev/null +++ b/Nio/Conversations/RecentRoomsContainerView.swift @@ -0,0 +1,160 @@ +import SwiftUI +import MatrixSDK +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: MXRoom.MXRoomId? + @State private var searchText: String = "" + + private func autoselectFirstRoom() { + /*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 { + 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. + .environmentObject(self.store) + .accentColor(accentColor) + } + .onAppear { + self.store.startListeningForRoomEvents() + self.restoreChat() + if #available(macOS 11, *) { autoselectFirstRoom() } + } + .onChange(of: appDelegate.selectedRoom) { newRoom in + selectedRoomId = newRoom + } + .searchable(text: $searchText) + } +} + + + +struct RoomsListSection: View { + let sectionHeader: String? + let rooms: [NIORoom] + let onLeaveAlertTitle: String + + @Binding var selectedRoomId: MXRoom.MXRoomId? + + @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] + } + + // 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) + } + } + .onDelete(perform: setLeaveIndex) + } + + @ViewBuilder + @MainActor + 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: MXRoom.MXRoomId? + + 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..aaeb5627 100644 --- a/Nio/Conversations/RecentRoomsView.swift +++ b/Nio/Conversations/RecentRoomsView.swift @@ -1,93 +1,39 @@ +// +// 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: 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} - } - - #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) - } - } - } + rooms.filter { + $0.room.summary.membership == .invite && + (searchText.isEmpty ? true : $0.displayName.lowercased().contains(searchText.lowercased())) } } - #else // iOS + private var settingsButton: some View { Button(action: { self.selectedNavigationItem = .settings @@ -141,104 +87,12 @@ 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 { - RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), rooms: []) + RecentRoomsView(selectedNavigationItem: .constant(nil), selectedRoomId: .constant(nil), searchText: .constant(""), rooms: []) } } diff --git a/Nio/Conversations/RoomContainerView.swift b/Nio/Conversations/RoomContainerView.swift new file mode 100644 index 00000000..554a4074 --- /dev/null +++ b/Nio/Conversations/RoomContainerView.swift @@ -0,0 +1,96 @@ +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 + asyncDetached { + await 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 + asyncDetached { + await 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/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 3040699c..e3a936df 100644 --- a/Nio/Conversations/RoomView.swift +++ b/Nio/Conversations/RoomView.swift @@ -1,130 +1,12 @@ -import SwiftUI import Combine import MatrixSDK +import SwiftUI -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) - } - ) - } +import Intents +import CoreSpotlight +import CoreServices - #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 -} +import NioKit struct RoomView: View { @Environment(\.userId) private var userId @@ -149,43 +31,45 @@ struct RoomView: View { @State private var attributedMessage = NSAttributedString(string: "") @State private var shouldPaginate = false - + @State private var canScrollFurther = true + 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 { 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), 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) } - + if #available(macOS 11, *) { Divider() } - + if areOtherUsersTyping { TypingIndicatorContainerView() } - + MessageComposerView( showAttachmentPicker: $showAttachmentPicker, isEditing: $isEditingMessage, @@ -194,12 +78,27 @@ struct RoomView: View { onCancel: cancelEdit, onCommit: send ) - .padding(.horizontal) - .padding(.bottom, 10) + .gesture(DragGesture(minimumDistance: 0.3, coordinateSpace: .local) + .onEnded({ value in + if value.translation.height > 0 { + isEditingMessage = false + } + }) + ) + + .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 { + await paginate(topEvent: topEvent) + } + } + } + .onAppear { + asyncDetached { + await room.createPagination() } } .alert(item: $eventToRedact) { eventId in @@ -208,8 +107,37 @@ struct RoomView: View { primaryButton: .destructive(Text(verbatim: L10n.Room.Remove.action), action: { self.onRedact(eventId, nil) }), secondaryButton: .cancel()) } + .userActivity("org.matrix.room") { 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 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/Extensions/INPreferences+async.swift b/Nio/Extensions/INPreferences+async.swift new file mode 100644 index 00000000..7969afeb --- /dev/null +++ b/Nio/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) }) + } + } +} diff --git a/Nio/Info.plist b/Nio/Info.plist index b22410e7..f6a2595b 100644 --- a/Nio/Info.plist +++ b/Nio/Info.plist @@ -62,13 +62,26 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + DevelopmentTeam + $(DEVELOPMENT_TEAM) + LSApplicationCategoryType + public.app-category.social-networking LSRequiresIPhoneOS + NSSiriUsageDescription + Send Messages via Siri + NSUserActivityTypes + + INSendMessageIntent + org.matrix.room + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes + UIBackgroundModes + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -81,8 +94,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - DevelopmentTeam - $(DEVELOPMENT_TEAM) UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 9e4cc291..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,13 +25,15 @@ 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 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 { + await 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() { + private func createRoom() async { isWaiting = true let parameters = MXRoomCreationParameters() @@ -162,10 +173,10 @@ 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 + 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..c0eefae2 100644 --- a/Nio/Nio.entitlements +++ b/Nio/Nio.entitlements @@ -2,6 +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 150379c7..164827d3 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 { - @StateObject private var accountStore = AccountStore() + #if os(macOS) + #else + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + #endif + + @StateObject private var accountStore = AccountStore.shared + + //@State private var selectedRoomId: ObjectIdentifier? @AppStorage("accentColor") private var accentColor: Color = .purple @@ -19,6 +28,16 @@ struct NioApp: App { RootView() .environmentObject(accountStore) .accentColor(accentColor) + .onContinueUserActivity("org.matrix.room", 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)") + }*/ + }) #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 new file mode 100644 index 00000000..6340e93f --- /dev/null +++ b/Nio/Settings/SettingsContainerView.swift @@ -0,0 +1,147 @@ +// +// 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 { + SettingsView(logoutAction: { + async { + await self.store.logout() + } + }) + } +} + +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, body: String? = nil, identifier: String) { + // TODO: fallback if notifications is disabled + print("trying to show banner") + asyncDetached { + let notification = UNMutableNotificationContent() + notification.title = text + if let body = body { + notification.body = body + } + 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 { + 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) + } + } + + Picker(selection: $appIconTitle.current, label: Text(verbatim: L10n.Settings.appIcon)) { + ForEach(AppIconTitle.alternatives) { AppIcon(title: $0) } + } + } + + Section { + Button(action: self.logoutAction) { + 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") + } + + 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) + } + } + } + } + .navigationBarTitle(L10n.Settings.title, displayMode: .inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(L10n.Settings.dismiss) { + presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} + + +struct SettingsContainerView_Previews: PreviewProvider { + static var previews: some View { + SettingsContainerView() + } +} diff --git a/Nio/Settings/SettingsView.swift b/Nio/Settings/SettingsView.swift deleted file mode 100644 index 636c51d1..00000000 --- a/Nio/Settings/SettingsView.swift +++ /dev/null @@ -1,98 +0,0 @@ -import SwiftUI -import NioKit - -struct SettingsContainerView: View { - @EnvironmentObject var store: AccountStore - - var body: some View { - #if os(macOS) - MacSettingsView(logoutAction: self.store.logout) - #else - SettingsView(logoutAction: 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) - } -} - -private struct SettingsView: View { - @AppStorage("accentColor") private var accentColor: Color = .purple - @StateObject private var appIconTitle = AppIconTitle() - let logoutAction: () -> Void - - @Environment(\.presentationMode) private var presentationMode - - var body: some View { - NavigationView { - 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) - } - } - - Picker(selection: $appIconTitle.current, label: Text(verbatim: L10n.Settings.appIcon)) { - ForEach(AppIconTitle.alternatives) { AppIcon(title: $0) } - } - } - - Section { - Button(action: self.logoutAction) { - Text(verbatim: L10n.Settings.logOut) - } - } - } - .navigationBarTitle(L10n.Settings.title, displayMode: .inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(L10n.Settings.dismiss) { - presentationMode.wrappedValue.dismiss() - } - } - } - } - } -} - -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView(logoutAction: {}) - } -} 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 diff --git a/Nio/Shared Views/ReverseList.swift b/Nio/Shared Views/ReverseList.swift index 503a3eaf..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,16 +36,18 @@ 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) { - hasReachedTop = $0 + if $0 != hasReachedTop { hasReachedTop = $0 } } } .scaleEffect(x: -1.0, y: 1.0) diff --git a/NioIntentsExtension/Info.plist b/NioIntentsExtension/Info.plist new file mode 100644 index 00000000..92b68e9d --- /dev/null +++ b/NioIntentsExtension/Info.plist @@ -0,0 +1,43 @@ + + + + + DevelopmentTeam + $(DEVELOPMENT_TEAM) + AppGroup + $(APPGROUP) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + INSearchForMessagesIntent + INSetMessageAttributeIntent + + IntentsRestrictedWhileProtectedDataUnavailable + + INSearchForMessagesIntent + INSetMessageAttributeIntent + + IntentsSupported + + INSearchForMessagesIntent + INSendMessage + INSendMessageIntent + INSetMessageAttributeIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + NSUserActivityTypes + + chat.nio.chat + + + diff --git a/NioIntentsExtension/IntentHandler.swift b/NioIntentsExtension/IntentHandler.swift new file mode 100644 index 00000000..11ae9d95 --- /dev/null +++ b/NioIntentsExtension/IntentHandler.swift @@ -0,0 +1,220 @@ +// +// IntentHandler.swift +// NioIntentsExtension +// +// Created by Finn Behrens on 15.06.21. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +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. +// 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). + @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 store.loginState.isAuthenticating { + // FIXME: !!!!!!! + #warning("this is not good coding!!!!!") + await Task.yield() + //print("logging in") + //sleep(1) + } + + var resolutionResults = [INSendMessageRecipientResolutionResult]() + + print("group name: \(String(describing: 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 + 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) + return resolutionResults + } else { + return [INSendMessageRecipientResolutionResult.needsValue()] + //completion([INSendMessageRecipientResolutionResult.needsValue()]) + }*/ + } + + public func resolveContent(for intent: INSendMessageIntent) async -> INStringResolutionResult { + if let text = intent.content, !text.isEmpty { + 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) 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) + return response + } + + // Handle the completed intent (required). + + public func handle(intent: INSendMessageIntent) async -> INSendMessageIntentResponse { + print("handle INSendMessageIntent") + // Implement your application logic to send a message here. + let store = await AccountStore.shared + + guard let recipient = intent.recipients?.first?.customIdentifier else { + return INSendMessageIntentResponse(code: .failure, userActivity: nil) + } + let userActivity = NSUserActivity(activityType: "org.matrix.room") + 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 response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) + 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) async -> INSearchForMessagesIntentResponse { + // 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)] + )] + return response + } + + // MARK: - INSetMessageAttributeIntentHandling + + 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) + return 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..adfabe7f --- /dev/null +++ b/NioIntentsExtension/NioIntentsExtension.entitlements @@ -0,0 +1,18 @@ + + + + + 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/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..92cac3f1 --- /dev/null +++ b/NioIntentsExtensionUI/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.intents-ui-service + + NSUserActivityTypes + + chat.nio.chat + + + diff --git a/NioIntentsExtensionUI/IntentViewController.swift b/NioIntentsExtensionUI/IntentViewController.swift new file mode 100644 index 00000000..10e0531e --- /dev/null +++ b/NioIntentsExtensionUI/IntentViewController.swift @@ -0,0 +1,39 @@ +// +// 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 { + + @IBOutlet var label: UILabel? + + 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..225aa48b --- /dev/null +++ b/NioIntentsExtensionUI/NioIntentsExtensionUI.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + keychain-access-groups + + + diff --git a/NioKit/Extensions/MX+Identifiable.swift b/NioKit/Extensions/MX+Identifiable.swift index 1fce4e0e..8f12c9fc 100644 --- a/NioKit/Extensions/MX+Identifiable.swift +++ b/NioKit/Extensions/MX+Identifiable.swift @@ -1,6 +1,78 @@ -import SwiftUI 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 MXEvent: 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 { + 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/MXAutoDiscovery+Async.swift b/NioKit/Extensions/MXAutoDiscovery+Async.swift new file mode 100644 index 00000000..8850a922 --- /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 + +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/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/Extensions/MXRestClient+Async.swift b/NioKit/Extensions/MXRestClient+Async.swift new file mode 100644 index 00000000..72069f6b --- /dev/null +++ b/NioKit/Extensions/MXRestClient+Async.swift @@ -0,0 +1,71 @@ +// +// 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 { + try await withCheckedThrowingContinuation { continuation in + self.login(type: loginType, username: username, password: password, 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 wellKnown() async throws -> MXWellKnown { + try await withCheckedThrowingContinuation { continuation in + 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 new file mode 100644 index 00000000..24ccad9d --- /dev/null +++ b/NioKit/Extensions/MXRoom+Async.swift @@ -0,0 +1,82 @@ +// +// MXRoom+Asnyc.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +import MatrixSDK + +extension MXRoom { + func members() async throws -> MXRoomMembers? { + try await withCheckedThrowingContinuation { continuation in + self.members(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)) + } + }) + } + } + + var liveTimeline: MXEventTimeline { + get async { + await withCheckedContinuation { continuation in + self.liveTimeline { continuation.resume(returning: $0!) } + } + } + } + + @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)) + } + }) + } + } + + @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 new file mode 100644 index 00000000..44df9a7c --- /dev/null +++ b/NioKit/Extensions/MXRoomMember+INPerson.swift @@ -0,0 +1,80 @@ +// +// MXRoomMember+INPerson.swift +// Masui +// +// Created by Finn Behrens on 11.06.21. +// + +import Foundation +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 { + 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 + displayName: self.displayname, + image: inImage, + contactIdentifier: nil, + customIdentifier: self.userId, + isMe: isMe, + suggestionType: .instantMessageAddress + ) + } +} + +extension MXRoomMembers { + var inPerson: [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 new file mode 100644 index 00000000..d40a8236 --- /dev/null +++ b/NioKit/Extensions/MXSession+Async.swift @@ -0,0 +1,68 @@ +// +// 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)) + } + } + } + } + + //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 { + let value: Any +} 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/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 1c8f588c..8a27d4eb 100644 --- a/NioKit/Models/NIORoom.swift +++ b/NioKit/Models/NIORoom.swift @@ -3,6 +3,11 @@ 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 { return lhs.displayName == rhs.displayName && @@ -20,8 +25,11 @@ public struct RoomItem: Codable, Hashable { } } +@MainActor public class NIORoom: ObservableObject { - public var room: MXRoom + static let logger = Logger(subsystem: "chat.nio", category: "ROOM") + + public nonisolated let room: MXRoom @Published public var summary: NIORoomSummary @@ -29,10 +37,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 +84,8 @@ public class NIORoom: ObservableObject { } } + + // MARK: - init public init(_ room: MXRoom) { self.room = room self.summary = NIORoomSummary(room.summary) @@ -72,10 +105,88 @@ 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") } } + + @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 + } + 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 = 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: "org.matrix.room") + 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) + // TODO: remove? + 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()) @@ -83,26 +194,60 @@ public class NIORoom: ObservableObject { // MARK: Sending Events - public func send(text: String) { + public func send(text: String, publishIntent: Bool = true) async { guard !text.isEmpty else { return } objectWillChange.send() // room.outgoingMessages() will change var localEcho: MXEvent? = nil - 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() + + 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 { + 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 - 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 } @@ -111,28 +256,62 @@ 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 } } - 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 - 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() + + 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] { + 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() @@ -142,10 +321,116 @@ public class NIORoom: ObservableObject { objectWillChange.send() // room.outgoingMessages() will change room.removeOutgoingMessage(event.eventId) } + + // intent + @available(*, deprecated, message: "Prefer `createNotification` to create an intent and donate that response") + private func donateOutgoingIntent(_ text: String? = nil) async { + do { + 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: senderId, 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? + + public func paginate(_ event: MXEvent, direction: MXTimelineDirection = .backwards, numItems: UInt = 40) async -> Bool { + if timeline == nil { + return await createPagination() + } + + 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 -> Bool { + guard timeline == nil else { + Self.logger.critical("createPagination called while a timeline already exists. This could be a critical bug") + return false + } + Self.logger.debug("Bootstraping pagination") + + timeline = await room.liveTimeline + timeline?.resetPagination() + 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 + } + } + + public func findEvent(id: MXEvent.MXEventId) -> MXEvent? { + self.eventCache.filter({ $0.id == id }).first + } } extension NIORoom: Identifiable { - public var id: ObjectIdentifier { + public nonisolated var id: MXRoom.MXRoomId { 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..3cc6d4a5 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -1,16 +1,39 @@ -import Foundation import Combine -import MatrixSDK +import Foundation import KeychainAccess +import Intents +import MatrixSDK +import os public enum LoginState { case loggedOut case authenticating case failure(Error) case loggedIn(userId: String) + + public var isAuthenticating: Bool { + switch self { + case .authenticating: + return true + default: + return false + } + } + + public func waitForLogin() async { + while self.isAuthenticating { + print("trying to authenticate") + //await Task.sleep(20_000) + } + } } +@MainActor public class AccountStore: ObservableObject { + static let logger = Logger(subsystem: "chat.nio.chat", category: "AccountStore") + + public static nonisolated let shared = AccountStore() + public var client: MXRestClient? public var session: MXSession? @@ -19,136 +42,134 @@ 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() { + public nonisolated init() { if CommandLine.arguments.contains("-clear-stored-credentials") { print("🗑 cleared stored credentials from keychain") MXCredentials .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() + } + } + + let developmentTeam = Bundle.main.infoDictionary?["DevelopmentTeam"] as? String 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 + async { + 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 @Published public var loginState: LoginState = .loggedOut - public func login(username: String, password: String, homeserver: URL) { - 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)") + public func login(username: String, password: String, homeserver: URL) async { + loginState = .authenticating + + client = MXRestClient(homeServer: homeserver, unrecognizedCertificateHandler: nil) + client?.acceptableContentTypes = ["text/plain", "text/html", "application/json", "application/octet-stream", "any"] + + do { + let credentials = try await client?.login(username: username, password: password) + guard let credentials = credentials else { + loginState = .failure(AccountStoreError.noCredentials) + return } + self.credentials = credentials + credentials.save(to: keychain) + loginState = try await sync() + session?.crypto.warnOnUnknowDevices = false + + try await self.setPusher() + } catch { + loginState = .failure(error) } } - public func logout(completion: @escaping (Result) -> Void) { - self.credentials?.clear(from: keychain) + public func logout() async { + 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 session?.logout() + try await self.setPusher(enable: false) + loginState = .loggedOut + } catch { + // Close the session even if the logout request failed + loginState = .loggedOut } + await NSUserActivity.deleteAllSavedUserActivities() } - - 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 - } + + 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") private func sync(completion: @escaping (Result) -> Void) { - guard let credentials = self.credentials else { return } - - 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): + async { + do { + let result = try await sync() + completion(.success(result)) + } catch { 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)") } } } + private func sync() async throws -> LoginState { + guard let credentials = self.credentials else { + throw AccountStoreError.noCredentials + } + + client = MXRestClient(credentials: credentials, unrecognizedCertificateHandler: nil) + session = MXSession(matrixRestClient: client!) + fileStore = MXFileStore() + + try await session!.setStore(fileStore!) + try await session?.start() + return .loggedIn(userId: credentials.userId!) + } + // MARK: - Rooms var listenReference: Any? 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) @@ -175,6 +196,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) } @@ -188,6 +213,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 @@ -200,4 +226,82 @@ public class AccountStore: ObservableObject { self.objectWillChange.send() } } + + // MARK: - Push Notifications + 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) + } + + 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 { + 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..105c1327 --- /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) { + async { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + print("didReceive") + print(bestAttemptContent?.userInfo as Any) + if let bestAttemptContent = bestAttemptContent { + let store = await 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) + 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 + } + */ + +} 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) }