Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5.3k
feat(swift-ios): share into durable composer drafts#5976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
26cc004b88b5712685dfdca1cbc65dbd21b6ddf260ec706e8ac7580a1e48c3314b0a1b30d61e2e146035bae3cab85302a2a5d41d48af35612490316a6e9253a110da4File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,28 @@ | ||
| import SwiftUI | ||
| @MainActor | ||
| final class PlatformIncomingShareRoutingGate { | ||
| private var isRouting = false | ||
| private var pendingOperation: (@MainActor () async -> Void)? | ||
| func request(_ operation: @escaping @MainActor () async -> Void) async { | ||
| pendingOperation = operation | ||
| guard !isRouting else { return } | ||
| isRouting = true | ||
| while let operation = pendingOperation { | ||
| pendingOperation = nil | ||
| await operation() | ||
| } | ||
| isRouting = false | ||
| } | ||
| } | ||
| enum PlatformIncomingShareDestinationConnectionPolicy { | ||
| static func canResolveThread(connectionState: FeatureConnection.State?) -> Bool { | ||
| connectionState == .connected | ||
| } | ||
| } | ||
| struct PlatformRootView: View { | ||
| @SwiftUI.Environment(\.scenePhase) private var scenePhase | ||
| @Bindable private var model: FeatureRootModel | ||
| @@ -11,6 +34,8 @@ struct PlatformRootView: View { | ||
| @State private var incomingShareCoordinator = PlatformIncomingShareCoordinator() | ||
| @State private var incomingShareNeedsProject = false | ||
| @State private var importedShareProjectID: String? | ||
| @State private var stagedIncomingShareID: String? | ||
| @State private var incomingShareRoutingGate = PlatformIncomingShareRoutingGate() | ||
| @State private var recentThreadsPersistenceTask: Task<Void, Never>? | ||
| init(model: FeatureRootModel) { | ||
| @@ -24,6 +49,15 @@ struct PlatformRootView: View { | ||
| onNavigationRequestConsumed: { requestID in | ||
| guard navigationRequest?.id == requestID else { return } | ||
| navigationRequest = nil | ||
| }, | ||
| acknowledgeIncomingShare: { shareID in | ||
| await acknowledgeStagedIncomingShare(id: shareID) | ||
| }, | ||
| releaseIncomingSharePresentation: { shareID in | ||
| if stagedIncomingShareID?.caseInsensitiveCompare(shareID) == .orderedSame { | ||
| stagedIncomingShareID = nil | ||
| incomingShareCoordinator.dismissStagedNewThread(id: shareID) | ||
| } | ||
| } | ||
| ) | ||
| .onOpenURL { url in | ||
| @@ -68,9 +102,19 @@ struct PlatformRootView: View { | ||
| synchronizeAgentAwareness() | ||
| synchronizeCloudDelivery() | ||
| } | ||
| .onChange(of: model.snapshot.settings.appearance, initial: true) { _, appearance in | ||
| T3SharedAppearanceStore.shared.update(appearance.sharedAppearance) | ||
| } | ||
| .onChange(of: model.snapshot.projects.map(\.id)) { _, _ in | ||
| refreshIncomingShares() | ||
| } | ||
| .onChange(of: incomingShareConnectionStates) { _, _ in | ||
| guard incomingShareCoordinator.pendingEnvelope?.destination?.kind == .existingThread, | ||
| !incomingShareCoordinator.isImporting else { return } | ||
| Task { @MainActor in | ||
| await routePendingIncomingShareIfNeeded() | ||
| } | ||
| } | ||
| .sheet(item: presentedIncomingShare, onDismiss: openImportedShareDraft) { envelope in | ||
| PlatformIncomingShareDestinationSheet( | ||
| envelope: envelope, | ||
| @@ -106,6 +150,9 @@ struct PlatformRootView: View { | ||
| Binding( | ||
| get: { | ||
| guard !incomingShareProjects.isEmpty else { return nil } | ||
| guard incomingShareCoordinator.pendingEnvelope?.destination == nil else { | ||
| return nil | ||
| } | ||
| return incomingShareCoordinator.pendingEnvelope | ||
| }, | ||
| set: { value in | ||
| @@ -190,13 +237,101 @@ struct PlatformRootView: View { | ||
| ) | ||
| } | ||
| private func refreshIncomingShares() { | ||
| private func refreshIncomingShares(preferredID: String? = nil) { | ||
| guard !model.isLoading else { return } | ||
| let hasProjects = !incomingShareProjects.isEmpty | ||
| Task { @MainActor in | ||
| if await incomingShareCoordinator.refresh(hasProjects: hasProjects) { | ||
| if await incomingShareCoordinator.refresh( | ||
| preferredID: preferredID, | ||
| hasProjects: hasProjects | ||
| ) { | ||
| incomingShareNeedsProject = true | ||
| } | ||
| await routePendingIncomingShareIfNeeded() | ||
| } | ||
| } | ||
| @MainActor | ||
| private func routePendingIncomingShareIfNeeded() async { | ||
| await incomingShareRoutingGate.request { | ||
| await performPendingIncomingShareRoute() | ||
| } | ||
| } | ||
| private func performPendingIncomingShareRoute() async { | ||
| guard let envelope = incomingShareCoordinator.pendingEnvelope, | ||
| let destination = envelope.destination, | ||
| stagedIncomingShareID != envelope.id else { return } | ||
| do { | ||
| switch destination.kind { | ||
| case .newThread: | ||
| guard let shareID = try await incomingShareCoordinator.stagePendingForNewThread() | ||
| else { return } | ||
| stagedIncomingShareID = shareID | ||
| navigationRequest = FeatureWorkspaceNavigationRequest( | ||
| destination: .sharedNewTask(shareID: shareID) | ||
| ) | ||
| case .existingThread: | ||
| guard let threadID = destination.threadID else { | ||
| try await incomingShareCoordinator.requestAnotherDestination() | ||
| model.errorMessage = "That thread is no longer available. Choose another destination." | ||
| return | ||
| } | ||
| if let environmentID = destination.environmentID, | ||
| !model.snapshot.environments.contains(where: { $0.id == environmentID }) { | ||
| try await incomingShareCoordinator.requestAnotherDestination() | ||
| model.errorMessage = "That thread's environment is no longer saved. Choose another destination." | ||
| return | ||
| } | ||
| guard await enableEnvironmentIfNeeded(destination.environmentID) else { | ||
| // Keep the saved destination so a temporarily unavailable | ||
| // environment can retry when its connection recovers. | ||
| return | ||
saphid marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| guard incomingShareDestinationIsConnected(destination.environmentID) else { | ||
| // A disconnected snapshot cannot authoritatively prove that | ||
| // the saved thread disappeared. Keep it queued for retry. | ||
| return | ||
| } | ||
| guard let thread = PlatformRouteResolver.thread( | ||
| in: model.snapshot, | ||
| environmentID: destination.environmentID, | ||
| id: threadID | ||
| ) else { | ||
| try await incomingShareCoordinator.requestAnotherDestination() | ||
| model.errorMessage = "That thread is no longer available. Choose another destination." | ||
| return | ||
| } | ||
| guard let imported = try await incomingShareCoordinator.importPending( | ||
saphid marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| into: thread | ||
| ) else { return } | ||
| navigationRequest = FeatureWorkspaceNavigationRequest( | ||
| destination: .sharedThread( | ||
| id: thread.id, | ||
| importDraft: imported.sharedContent | ||
| ) | ||
| ) | ||
saphid marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| PlatformHapticEngine.shared.emit( | ||
| .success, | ||
| enabled: model.snapshot.settings.hapticsEnabled | ||
| ) | ||
| } catch { | ||
| model.errorMessage = error.localizedDescription | ||
| } | ||
| } | ||
| @MainActor | ||
| private func acknowledgeStagedIncomingShare(id: String) async -> Bool { | ||
| do { | ||
| try await incomingShareCoordinator.acknowledgeStagedNewThread(id: id) | ||
| if stagedIncomingShareID?.caseInsensitiveCompare(id) == .orderedSame { | ||
| stagedIncomingShareID = nil | ||
| } | ||
| return true | ||
| } catch { | ||
| model.errorMessage = error.localizedDescription | ||
| return false | ||
| } | ||
| } | ||
| @@ -307,6 +442,20 @@ struct PlatformRootView: View { | ||
| return await model.setEnvironmentEnabled(id, enabled: true) | ||
| } | ||
| private func incomingShareDestinationIsConnected(_ id: String?) -> Bool { | ||
| let state = id.flatMap { environmentID in | ||
| model.snapshot.environments.first(where: { $0.id == environmentID })?.connectionState | ||
| } ?? (id == nil ? model.snapshot.connection.state : nil) | ||
| return PlatformIncomingShareDestinationConnectionPolicy.canResolveThread( | ||
| connectionState: state | ||
| ) | ||
| } | ||
| private var incomingShareConnectionStates: [FeatureConnection.State?] { | ||
| [model.snapshot.connection.state] | ||
| + model.snapshot.environments.map(\.connectionState) | ||
| } | ||
| /// Home revisions are coalesced by FeatureRootModel, so this performs one | ||
| /// bounded scan per meaningful snapshot change rather than on every render. | ||
| private func processThreadChanges() { | ||
| @@ -345,3 +494,13 @@ struct PlatformRootView: View { | ||
| ) | ||
| } | ||
| } | ||
| private extension FeatureAppearance { | ||
| var sharedAppearance: T3SharedAppearance { | ||
| switch self { | ||
| case .system: .system | ||
| case .light: .light | ||
| case .dark: .dark | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.