diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 5407a038..7e452529 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -42,6 +42,8 @@ 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; }; 9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; }; 9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4B2952005C00B3281A /* MessagesTab.swift */; }; + 9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */; }; + 9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */; }; 9F38A7332ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; }; 9F38A7342ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; }; 9F38A7352ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; }; @@ -196,6 +198,8 @@ 9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = ""; }; 9F35DB4829506F7F00B3281A /* Notifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Notifications; path = Packages/Notifications; sourceTree = ""; }; 9F35DB4B2952005C00B3281A /* MessagesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTab.swift; sourceTree = ""; }; + 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostIntent.swift; sourceTree = ""; }; + 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentService.swift; sourceTree = ""; }; 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = ""; }; 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRegistry.swift; sourceTree = ""; }; @@ -349,6 +353,15 @@ path = IceCubesNotifications; sourceTree = ""; }; + 9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */ = { + isa = PBXGroup; + children = ( + 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */, + 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */, + ); + path = IceCubesAppIntents; + sourceTree = ""; + }; 9F398AB429360A5800A889F2 /* App */ = { isa = PBXGroup; children = ( @@ -454,6 +467,7 @@ DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */, 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */, 9FBFE63B292A715500C250E9 /* IceCubesApp */, + 9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */, E9DF41FD29830FEC0003AAD2 /* IceCubesActionExtension */, 9F2A5417296AB631009B2D7C /* IceCubesNotifications */, 9FAD858929743F7400496AB1 /* IceCubesShareExtension */, @@ -828,6 +842,8 @@ 9F15D6042B3DC2180008C220 /* NavigationSheet.swift in Sources */, 9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */, 9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */, + 9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */, + 9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */, 9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9FC14EF42B494D940006CEE1 /* RemoteTimelinesSettingView.swift in Sources */, diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift index 8edfec18..4c8b6c8e 100644 --- a/IceCubesApp/App/AppRegistry.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -85,7 +85,10 @@ extension View { StatusEditor.MainView(mode: .replyTo(status: status)) .withEnvironments() case let .newStatusEditor(visibility): - StatusEditor.MainView(mode: .new(visibility: visibility)) + StatusEditor.MainView(mode: .new(text: nil, visibility: visibility)) + .withEnvironments() + case let .prefilledStatusEditor(text, visibility): + StatusEditor.MainView(mode: .new(text: text, visibility: visibility)) .withEnvironments() case let .editStatusEditor(status): StatusEditor.MainView(mode: .edit(status: status)) diff --git a/IceCubesApp/App/Main/IceCubesApp+Scene.swift b/IceCubesApp/App/Main/IceCubesApp+Scene.swift index 27e8f5a4..957d7c48 100644 --- a/IceCubesApp/App/Main/IceCubesApp+Scene.swift +++ b/IceCubesApp/App/Main/IceCubesApp+Scene.swift @@ -2,6 +2,7 @@ import Env import MediaUI import StatusKit import SwiftUI +import AppIntents extension IceCubesApp { var appScene: some Scene { @@ -22,6 +23,7 @@ extension IceCubesApp { .environment(theme) .environment(watcher) .environment(pushNotificationsService) + .environment(appIntentService) .environment(\.isSupporter, isSupporter) .sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in MediaUIView(selectedAttachment: selectedMediaAttachment, @@ -47,6 +49,12 @@ extension IceCubesApp { } } } + .onChange(of: appIntentService.handledIntent) { _, _ in + if let intent = appIntentService.handledIntent?.intent { + handleIntent(intent) + appIntentService.handledIntent = nil + } + } .withModelContainer() } #if targetEnvironment(macCatalyst) @@ -74,7 +82,9 @@ extension IceCubesApp { Group { switch destination.wrappedValue { case let .newStatusEditor(visibility): - StatusEditor.MainView(mode: .new(visibility: visibility)) + StatusEditor.MainView(mode: .new(text: nil, visibility: visibility)) + case let .prefilledStatusEditor(text, visibility): + StatusEditor.MainView(mode: .new(text: text, visibility: visibility)) case let .editStatusEditor(status): StatusEditor.MainView(mode: .edit(status: status)) case let .quoteStatusEditor(status): @@ -115,4 +125,16 @@ extension IceCubesApp { .defaultSize(width: 1200, height: 1000) .windowResizability(.contentMinSize) } + + private func handleIntent(_ intent: any AppIntent) { + if let postIntent = appIntentService.handledIntent?.intent as? PostIntent { + #if os(visionOS) || os(macOS) + openWindow(value: WindowDestinationEditor.prefilledStatusEditor(text: postIntent.content ?? "", + visibility: userPreferences.postVisibility)) + #else + appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "", + visibility: userPreferences.postVisibility) + #endif + } + } } diff --git a/IceCubesApp/App/Main/IceCubesApp.swift b/IceCubesApp/App/Main/IceCubesApp.swift index e9205314..cc64b64d 100644 --- a/IceCubesApp/App/Main/IceCubesApp.swift +++ b/IceCubesApp/App/Main/IceCubesApp.swift @@ -23,6 +23,7 @@ struct IceCubesApp: App { @State var currentAccount = CurrentAccount.shared @State var userPreferences = UserPreferences.shared @State var pushNotificationsService = PushNotificationsService.shared + @State var appIntentService = AppIntentService.shared @State var watcher = StreamWatcher.shared @State var quickLook = QuickLook.shared @State var theme = Theme.shared diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 452d1540..42d3c9c1 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -39936,6 +39936,12 @@ } } } + }, + "Post content" : { + + }, + "Post to Mastodon" : { + }, "report.action.send" : { "localizations" : { @@ -80466,6 +80472,9 @@ } } } + }, + "Use Ice Cubes to post text to Mastodon" : { + } }, "version" : "1.0" diff --git a/IceCubesAppIntents/AppIntentService.swift b/IceCubesAppIntents/AppIntentService.swift new file mode 100644 index 00000000..6de561aa --- /dev/null +++ b/IceCubesAppIntents/AppIntentService.swift @@ -0,0 +1,25 @@ +import SwiftUI +import AppIntents + +@Observable +public class AppIntentService: @unchecked Sendable { + struct HandledIntent: Equatable { + static func == (lhs: AppIntentService.HandledIntent, rhs: AppIntentService.HandledIntent) -> Bool { + lhs.id == rhs.id + } + + let id: String + let intent: any AppIntent + + init(intent: any AppIntent) { + self.id = UUID().uuidString + self.intent = intent + } + } + + public static let shared = AppIntentService() + + var handledIntent: HandledIntent? + + private init() { } +} diff --git a/IceCubesAppIntents/PostIntent.swift b/IceCubesAppIntents/PostIntent.swift new file mode 100644 index 00000000..e1b5e361 --- /dev/null +++ b/IceCubesAppIntents/PostIntent.swift @@ -0,0 +1,20 @@ +import Foundation +import AppIntents + +struct PostIntent: AppIntent { + static let title: LocalizedStringResource = "Post to Mastodon" + static var description: IntentDescription { + get { + "Use Ice Cubes to post text to Mastodon" + } + } + static let openAppWhenRun: Bool = true + + @Parameter(title: "Post content") + var content: String? + + func perform() async throws -> some IntentResult { + AppIntentService.shared.handledIntent = .init(intent: self) + return .result() + } +} diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 0a6fc5e3..06cdcd25 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -31,6 +31,7 @@ public enum RouterDestination: Hashable { public enum WindowDestinationEditor: Hashable, Codable { case newStatusEditor(visibility: Models.Visibility) + case prefilledStatusEditor(text: String, visibility: Models.Visibility) case editStatusEditor(status: Status) case replyToStatusEditor(status: Status) case quoteStatusEditor(status: Status) @@ -52,6 +53,7 @@ public enum SheetDestination: Identifiable, Hashable { } case newStatusEditor(visibility: Models.Visibility) + case prefilledStatusEditor(text: String, visibility: Models.Visibility) case editStatusEditor(status: Status) case replyToStatusEditor(status: Status) case quoteStatusEditor(status: Status) @@ -78,7 +80,7 @@ public enum SheetDestination: Identifiable, Hashable { public var id: String { switch self { case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, - .mentionStatusEditor, .quoteLinkStatusEditor: + .mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor: "statusEditor" case .listCreate: "listCreate" diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift index 89582e4e..c8cc7fb4 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift @@ -161,7 +161,7 @@ extension StatusEditor { Button { // all SEVM have the same visibility value - followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility))) + followUpSEVMs.append(ViewModel(mode: .new(text: nil, visibility: focusedSEVM.visibility))) } label: { Image(systemName: "arrowshape.turn.up.left.circle.fill") } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift index b466ab3d..2fe93ba3 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift @@ -5,7 +5,7 @@ import UIKit public extension StatusEditor.ViewModel { enum Mode { case replyTo(status: Status) - case new(visibility: Models.Visibility) + case new(text: String?, visibility: Models.Visibility) case edit(status: Status) case quote(status: Status) case quoteLink(link: URL) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index f91536bc..17638198 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -301,7 +301,11 @@ public extension StatusEditor { func prepareStatusText() { switch mode { - case let .new(visibility): + case let .new(text, visibility): + if let text { + statusText = .init(string: text) + selectedRange = .init(location: text.utf16.count, length: 0) + } self.visibility = visibility case let .shareExtension(items): itemsProvider = items @@ -557,7 +561,7 @@ public extension StatusEditor { !statusText.string.contains(url.absoluteString) { embeddedStatus = nil - mode = .new(visibility: visibility) + mode = .new(text: nil, visibility: visibility) } }