diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileHostPickerView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileHostPickerView.swift deleted file mode 100644 index e520af5941c2..000000000000 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileHostPickerView.swift +++ /dev/null @@ -1,202 +0,0 @@ -#if os(iOS) -import CMUXMobileCore -import CmuxMobilePairedMac -import CmuxMobileShell -import CmuxMobileSupport -import SwiftUI - -/// Lets the user switch which paired Mac this device controls, and pair another. -/// -/// Lists every Mac paired with this device (from the on-device store), marks the -/// one the live connection targets, switches on tap, hides on swipe, and pairs -/// a new Mac by scanning its QR code without dropping the others. -struct MobileHostPickerView: View { - @Bindable var store: CMUXMobileShellStore - @Environment(\.dismiss) private var dismiss - @State private var showingScanner = false - - var body: some View { - NavigationStack { - List { - Section { - if store.pairedMacs.isEmpty { - Text(L10n.string("mobile.hostPicker.empty", defaultValue: "No computers yet. Sign in to cmux on your computer with this account and it appears here automatically. If it does not, tap Pair Another Computer and scan its QR code.")) - .foregroundStyle(.secondary) - } - ForEach(store.pairedMacs) { mac in - macRow(mac) - } - } header: { - Text(L10n.string("mobile.hostPicker.header", defaultValue: "Paired Computers")) - } footer: { - Text(L10n.string( - "mobile.hostPicker.footer", - defaultValue: "Switch which computer this device controls. A computer joins this list automatically the first time this phone connects to it. Pair Another Computer adds more without removing the ones you have." - )) - } - - Section { - Button { - showingScanner = true - } label: { - Label( - L10n.string("mobile.hostPicker.addMac", defaultValue: "Pair Another Computer"), - systemImage: "plus" - ) - } - .accessibilityIdentifier("MobileHostPickerAddMac") - } - } - .navigationTitle(L10n.string("mobile.hostPicker.title", defaultValue: "Switch Computer")) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(L10n.string("mobile.common.done", defaultValue: "Done")) { - dismiss() - } - .accessibilityIdentifier("MobileHostPickerDone") - } - } - .task { await store.loadPairedMacs() } - .sheet(isPresented: $showingScanner) { - MobilePairingScannerSheet { code in - showingScanner = false - Task { - let result = await store.connectPairingURLResult( - code, - userEnteredPairingCode: true - ) - if result != .needsUserApproval { - await store.loadPairedMacs() - dismiss() - } - } - } - } - } - .alert( - L10n.string("mobile.pairing.versionWarningTitle", defaultValue: "Compatibility mismatch"), - isPresented: Binding( - get: { store.pairingVersionWarning != nil }, - set: { _ in } - ) - ) { - Button(L10n.string("mobile.common.cancel", defaultValue: "Cancel"), role: .cancel) { - store.cancelPairing() - } - Button( - L10n.string("mobile.pairing.versionWarningContinue", defaultValue: "Continue anyway"), - role: .destructive - ) { - Task { - let result = await store.acceptPairingVersionWarning() - if result != .needsUserApproval { - await store.loadPairedMacs() - dismiss() - } - } - } - } message: { - Text(store.pairingVersionWarning ?? "") - } - .accessibilityIdentifier("MobileHostPicker") - } - - @ViewBuilder - private func macRow(_ mac: MobilePairedMac) -> some View { - let isActive = mac.isActive - let appDisplayName = localizedAppDisplayName(for: mac) - let hasSiblingInstance = store.pairedMacs.contains { - $0.id != mac.id && $0.macDeviceID == mac.macDeviceID - } - Button { - Task { - await store.switchToMac( - macDeviceID: mac.macDeviceID, - instanceTag: mac.instanceTag - ) - } - } label: { - HStack(spacing: 12) { - Image(systemName: "desktopcomputer") - .foregroundStyle(.secondary) - VStack(alignment: .leading, spacing: 2) { - Text(scopedDisplayName(mac.resolvedName)) - .foregroundStyle(.primary) - if let appDisplayName { - Text(appDisplayName) - .font(.caption) - .foregroundStyle(.secondary) - } else if hasSiblingInstance { - Text(L10n.string("mobile.hostPicker.app.legacy", defaultValue: "Legacy")) - .font(.caption) - .foregroundStyle(.secondary) - } - Text(mac.lastSeenAt, format: .relative(presentation: .named)) - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer(minLength: 8) - if isActive { - Image(systemName: "checkmark") - .foregroundStyle(Color.accentColor) - .accessibilityLabel(L10n.string("mobile.hostPicker.active", defaultValue: "Active")) - } - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .accessibilityIdentifier("MobileHostPickerRow-\(mac.id)") - .swipeActions(edge: .trailing) { - Button { - Task { - await store.hideStoredMac( - macDeviceID: mac.macDeviceID, - instanceTag: mac.instanceTag - ) - } - } label: { - Label( - L10n.string("mobile.hostPicker.hide", defaultValue: "Hide"), - systemImage: "eye.slash" - ) - } - .accessibilityIdentifier("MobileHostPickerHide-\(mac.id)") - } - } - - private func scopedDisplayName(_ baseName: String) -> String { - MobileIOSBuildScope.current()?.computerDisplayName(baseName) ?? baseName - } - - private func localizedAppDisplayName(for mac: MobilePairedMac) -> String? { - guard let appName = MacBuildChannel().appDisplayName( - bundleID: nil, - tag: mac.instanceTag - ) else { - return nil - } - switch appName { - case "cmux": - return L10n.string("mobile.hostPicker.app.stable", defaultValue: "cmux") - case "cmux Nightly": - return L10n.string("mobile.hostPicker.app.nightly", defaultValue: "cmux Nightly") - case "cmux RC": - return L10n.string("mobile.hostPicker.app.rc", defaultValue: "cmux RC") - case "cmux Staging": - return L10n.string("mobile.hostPicker.app.staging", defaultValue: "cmux Staging") - case "cmux DEV": - return L10n.string("mobile.hostPicker.app.dev", defaultValue: "cmux DEV") - default: - let prefix = "cmux DEV " - guard appName.hasPrefix(prefix) else { return appName } - let tag = appName.dropFirst(prefix.count) - let format = L10n.string( - "mobile.hostPicker.app.devTaggedFormat", - defaultValue: "cmux DEV %@" - ) - return String(format: format, String(tag)) - } - } -} -#endif diff --git a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift index 6b23ea6dbbe2..482638c70d56 100644 --- a/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift +++ b/Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift @@ -28,8 +28,8 @@ struct MobileSettingsView: View { let connectedHostName: String let startPairingScanner: (() -> Void)? let signOut: (() -> Void)? - /// The shell store, used to drive the multi-Mac switcher. `nil` in previews, - /// where the "Switch Mac" entry is hidden. + /// The shell store, used for the live connection rows and the onboarding + /// replay's connection state. `nil` in previews. var store: CMUXMobileShellStore? @AppStorage(MobileSettingsView.sendAnonymousTelemetryKey) private var sendAnonymousTelemetry = false @@ -40,7 +40,6 @@ struct MobileSettingsView: View { /// `isEnabled` as a non-observable `UserDefaults` read, so reading it /// directly in `body` would not re-render when it flips. @State private var notificationsEnabled = false - @State private var showingHostPicker = false @State private var showingOnboarding = false @State private var showingSetupHelp = false #if DEBUG @@ -90,10 +89,11 @@ struct MobileSettingsView: View { } } - // Hidden entirely when there is nothing to show (no connected - // Mac and no store to switch with), so the no-devices screen's - // reuse of this sheet does not render an empty header. - if hasConnectionSection { + // Hidden when there is no live connection row to show, so the + // no-devices screen's reuse of this sheet does not render an + // empty header. Switching Macs lives in the workspace list's + // computer picker. + if hasConnectionRows { Section(L10n.string("mobile.settings.connection", defaultValue: "Connection")) { if let connections = store?.liveMacConnections, !connections.isEmpty { @@ -120,18 +120,9 @@ struct MobileSettingsView: View { value: connectedHostName ) } - if store != nil { - Button { - showingHostPicker = true - } label: { - Label( - L10n.string("mobile.settings.switchMac", defaultValue: "Switch Computer"), - systemImage: "macbook.and.iphone" - ) - } - .accessibilityIdentifier("MobileSettingsSwitchMac") - } } + } + if hasConnectionSection { Button { showingSetupHelp = true } label: { @@ -462,11 +453,6 @@ struct MobileSettingsView: View { ToastGalleryView() } #endif - .sheet(isPresented: $showingHostPicker) { - if let store { - MobileHostPickerView(store: store) - } - } .sheet(isPresented: $showingOnboarding) { // Re-entry never writes first-run progress. The final scene reads // live connection state and can reopen pairing from offline Settings. @@ -529,9 +515,15 @@ struct MobileSettingsView: View { nil } - /// Whether the Connection section has any rows to show. When this sheet is - /// reused from the no-devices screen there is no connected Mac or store to - /// switch with, so the section is omitted entirely. + /// Whether the Connection section has any rows to show. When nothing is + /// connected the section is omitted entirely so its header never sits empty. + private var hasConnectionRows: Bool { + store?.liveMacConnections.isEmpty == false || !connectedHostName.isEmpty + } + + /// Whether the setup and introduction entries apply. When this sheet is + /// reused from the no-devices screen there is no connected Mac or store, + /// so they are hidden. private var hasConnectionSection: Bool { !connectedHostName.isEmpty || store != nil } diff --git a/ios/cmux/Resources/Localizable.xcstrings b/ios/cmux/Resources/Localizable.xcstrings index 483decc5360c..fa4fb92afe24 100644 --- a/ios/cmux/Resources/Localizable.xcstrings +++ b/ios/cmux/Resources/Localizable.xcstrings @@ -2415,23 +2415,6 @@ } } }, - "mobile.connection.connectAttemptGated": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Mobile sync connection is retrying" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "モバイル同期接続を再試行しています" - } - } - } - }, "mobile.connection.connected": { "extractionState": "manual", "localizations": { @@ -3163,244 +3146,6 @@ } } }, - "mobile.hostPicker.app.dev": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux DEV" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux DEV" - } - } - } - }, - "mobile.hostPicker.app.devTaggedFormat": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux DEV %@" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux DEV %@" - } - } - } - }, - "mobile.hostPicker.app.legacy": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Legacy" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "旧形式" - } - } - } - }, - "mobile.hostPicker.app.nightly": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux Nightly" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux Nightly" - } - } - } - }, - "mobile.hostPicker.app.rc": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux RC" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux RC" - } - } - } - }, - "mobile.hostPicker.app.stable": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux" - } - } - } - }, - "mobile.hostPicker.app.staging": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux Staging" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux Staging" - } - } - } - }, - "mobile.hostPicker.active": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Active" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "アクティブ" - } - } - } - }, - "mobile.hostPicker.addMac": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Pair Another Computer" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "別のコンピュータをペアリング" - } - } - } - }, - "mobile.hostPicker.empty": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "No computers yet. Sign in to cmux on your computer with this account and it appears here automatically. If it does not, tap Pair Another Computer and scan its QR code." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コンピュータはまだありません。コンピュータのcmuxにこのアカウントでサインインすると、ここに自動的に表示されます。表示されない場合は、「別のコンピュータをペアリング」からQRコードをスキャンしてください。" - } - } - } - }, - "mobile.hostPicker.footer": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Switch which computer this device controls. A computer joins this list automatically the first time this phone connects to it. Pair Another Computer adds more without removing the ones you have." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "このデバイスが操作するコンピュータを切り替えます。コンピュータは、この端末が最初に接続したときに自動的にこの一覧に追加されます。「別のコンピュータをペアリング」で、今あるコンピュータを残したまま追加できます。" - } - } - } - }, - "mobile.hostPicker.hide": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Hide" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "非表示" - } - } - } - }, - "mobile.hostPicker.header": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Paired Computers" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ペアリング済みのコンピュータ" - } - } - } - }, - "mobile.hostPicker.title": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Switch Computer" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コンピュータを切り替え" - } - } - } - }, "mobile.macUpdateHint.browserStream": { "extractionState": "manual", "localizations": { @@ -7260,23 +7005,6 @@ } } }, - "mobile.settings.switchMac": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Switch Computer" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コンピュータを切り替え" - } - } - } - }, "mobile.settings.team": { "extractionState": "manual", "localizations": {