From 4db056ad7ad71e5dc26b0dbb9326f8086207e442 Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 31 Jul 2026 11:42:22 +0300 Subject: [PATCH] feat: wire alternate icons into settings, launch, and panic wipe Expose the picker in App Info, re-apply the stored preference on appear, and clear the disguise choice during a full data wipe. --- bitchat/BitchatApp.swift | 3 ++ bitchat/ViewModels/ChatViewModel.swift | 1 + bitchat/Views/AppInfoView.swift | 51 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index cf2e1bd1..569a164a 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -45,6 +45,9 @@ struct BitchatApp: App { .onAppear { appDelegate.runtime = runtime runtime.start() + #if os(iOS) + AlternateAppIconSettings.applyStoredPreference() + #endif } .onOpenURL { url in runtime.handleOpenURL(url) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 12adfda9..96328ad8 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -1637,6 +1637,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage MeshSightingsTracker.shared.clear() MeshEchoSettings.reset() NotificationPrivacySettings.reset() + AlternateAppIconSettings.reset() // A hand-added relay names an operator someone chose to route through, // which is the kind of trace a wipe should not leave behind. NostrRelaySettings.reset() diff --git a/bitchat/Views/AppInfoView.swift b/bitchat/Views/AppInfoView.swift index cb99fbac..6da98826 100644 --- a/bitchat/Views/AppInfoView.swift +++ b/bitchat/Views/AppInfoView.swift @@ -34,6 +34,9 @@ struct AppInfoView: View { /// The override changed this session; localization resolves at process /// start, so surface the restart hint. @State private var showLanguageRestartNote = false + #if os(iOS) + @State private var selectedAppIcon = AlternateAppIconSettings.selected + #endif private enum Pane: String { case settings @@ -61,6 +64,9 @@ struct AppInfoView: View { static let tabSettings = String(localized: "app_info.tab.settings", defaultValue: "settings", comment: "Segmented control label for the settings pane of the app info sheet") static let tabInfo = String(localized: "app_info.tab.info", defaultValue: "info", comment: "Segmented control label for the info pane of the app info sheet") + static let iconTitle = String(localized: "app_info.settings.icon.title", defaultValue: "HOME SCREEN ICON", comment: "Section header (uppercase) for alternate app icon picker") + static let iconSubtitle = String(localized: "app_info.settings.icon.subtitle", defaultValue: "optional low-profile icons. the real app name still shows in settings and search — this only changes the home-screen glyph.", comment: "Caption under the alternate app icon picker explaining the limits of disguise") + static let connectivityTitle = String(localized: "app_info.settings.connectivity.title", defaultValue: "CONNECTIVITY", comment: "Section header (uppercase) for the connectivity toggles: mesh bridge, internet gateway, tor routing") static let languageTitle = String(localized: "app_info.settings.language.title", defaultValue: "LANGUAGE", comment: "Section header (uppercase) for the app language picker in settings") @@ -357,6 +363,51 @@ struct AppInfoView: View { } } + #if os(iOS) + // Low-profile home-screen icons for the opsec / disguise path (#1447). + // macOS has no alternate-icon API equivalent, so this stays iOS-only. + VStack(alignment: .leading, spacing: 12) { + SectionHeader(verbatim: Strings.Settings.iconTitle) + + settingsCard { + Text(verbatim: Strings.Settings.iconSubtitle) + .bitchatFont(size: 11) + .foregroundColor(secondaryTextColor) + + HStack(spacing: 8) { + ForEach(AlternateAppIconSettings.Icon.allCases) { icon in + Button { + selectedAppIcon = icon + AlternateAppIconSettings.setSelected(icon, in: .standard, applySystem: true) + } label: { + Text(verbatim: icon.title) + .bitchatFont( + size: 12, + weight: selectedAppIcon == icon ? .semibold : .regular + ) + .foregroundColor( + selectedAppIcon == icon ? palette.accent : secondaryTextColor + ) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill( + selectedAppIcon == icon + ? palette.accent.opacity(0.15) + : Color.clear + ) + ) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityAddTraits(selectedAppIcon == icon ? .isSelected : []) + } + } + } + } + #endif + // Language — an in-app override so the UI language can differ // from the device language (takes effect on next launch). VStack(alignment: .leading, spacing: 12) {