From 9c01cd4fa12fd885b4404fafaf525bd283788860 Mon Sep 17 00:00:00 2001 From: Rod Bahmanyari Date: Wed, 22 Jul 2026 16:02:00 -0700 Subject: [PATCH] Header: add a gear button that opens settings, on the settings pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings were reachable only by tapping the "bitchat/" wordmark, which reads as static text — new users have no way to find it. Add a gearshape button at the right end of the header icon cluster that opens the App Info sheet directly on its settings pane: the sheet's pane memory is sticky and defaults to info, so a control labeled "settings" would otherwise land first-time users on the info tab. The logo tap stays as a shortcut and keeps the sticky last-used pane, and the deliberately undiscoverable triple-tap panic wipe is unchanged. The label/tooltip ships as a new content.header.settings key seeded with copies of the catalog's existing "settings" translations, so it is localized in all 30 supported languages from the start. Co-Authored-By: Claude Fable 5 --- bitchat/App/AppChromeModel.swift | 8 +++++- bitchat/Localizable.xcstrings | 36 +++++++++++++++++++++++++++ bitchat/Views/AppInfoView.swift | 14 +++++++++-- bitchat/Views/ContentHeaderView.swift | 27 +++++++++++++++++--- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/bitchat/App/AppChromeModel.swift b/bitchat/App/AppChromeModel.swift index 51cadfb0..e37615b1 100644 --- a/bitchat/App/AppChromeModel.swift +++ b/bitchat/App/AppChromeModel.swift @@ -80,7 +80,13 @@ final class AppChromeModel: ObservableObject { showingFingerprintFor = nil } - func presentAppInfo() { + /// Presents the App Info sheet. Pass a pane when the launching control + /// promises one (the header gear is labeled "settings"); nil keeps the + /// sheet's own sticky last-used pane. + func presentAppInfo(pane: AppInfoView.Pane? = nil) { + if let pane { + AppInfoView.setSelectedPane(pane) + } isAppInfoPresented = true } diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index e498a20e..ae8335a5 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -56139,6 +56139,42 @@ } } }, + "content.header.settings" : { + "comment" : "Accessibility label and tooltip for the header gear button that opens the settings/app info sheet", + "extractionState" : "manual", + "localizations" : { + "ar" : { "stringUnit" : { "state" : "translated", "value" : "الإعدادات" } }, + "bn" : { "stringUnit" : { "state" : "translated", "value" : "সেটিংস" } }, + "de" : { "stringUnit" : { "state" : "translated", "value" : "einstellungen" } }, + "en" : { "stringUnit" : { "state" : "translated", "value" : "settings" } }, + "es" : { "stringUnit" : { "state" : "translated", "value" : "ajustes" } }, + "fa" : { "stringUnit" : { "state" : "translated", "value" : "تنظیمات" } }, + "fil" : { "stringUnit" : { "state" : "translated", "value" : "settings" } }, + "fr" : { "stringUnit" : { "state" : "translated", "value" : "réglages" } }, + "he" : { "stringUnit" : { "state" : "translated", "value" : "הגדרות" } }, + "hi" : { "stringUnit" : { "state" : "translated", "value" : "सेटिंग्स" } }, + "id" : { "stringUnit" : { "state" : "translated", "value" : "pengaturan" } }, + "it" : { "stringUnit" : { "state" : "translated", "value" : "impostazioni" } }, + "ja" : { "stringUnit" : { "state" : "translated", "value" : "設定" } }, + "ko" : { "stringUnit" : { "state" : "translated", "value" : "설정" } }, + "ms" : { "stringUnit" : { "state" : "translated", "value" : "tetapan" } }, + "ne" : { "stringUnit" : { "state" : "translated", "value" : "सेटिङ" } }, + "nl" : { "stringUnit" : { "state" : "translated", "value" : "instellingen" } }, + "pl" : { "stringUnit" : { "state" : "translated", "value" : "ustawienia" } }, + "pt" : { "stringUnit" : { "state" : "translated", "value" : "definições" } }, + "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ajustes" } }, + "ru" : { "stringUnit" : { "state" : "translated", "value" : "настройки" } }, + "sv" : { "stringUnit" : { "state" : "translated", "value" : "inställningar" } }, + "ta" : { "stringUnit" : { "state" : "translated", "value" : "அமைப்புகள்" } }, + "th" : { "stringUnit" : { "state" : "translated", "value" : "ตั้งค่า" } }, + "tr" : { "stringUnit" : { "state" : "translated", "value" : "ayarlar" } }, + "uk" : { "stringUnit" : { "state" : "translated", "value" : "налаштування" } }, + "ur" : { "stringUnit" : { "state" : "translated", "value" : "سیٹنگز" } }, + "vi" : { "stringUnit" : { "state" : "translated", "value" : "cài đặt" } }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设置" } }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定" } } + } + }, "content.help.verification" : { "extractionState" : "manual", "localizations" : { diff --git a/bitchat/Views/AppInfoView.swift b/bitchat/Views/AppInfoView.swift index cb99fbac..7a56bd53 100644 --- a/bitchat/Views/AppInfoView.swift +++ b/bitchat/Views/AppInfoView.swift @@ -28,18 +28,28 @@ struct AppInfoView: View { @ObservedObject private var locationManager = LocationChannelManager.shared /// Sticky across opens: first-ever open lands on Info (the gentler /// introduction), and afterwards the sheet reopens wherever it was left. - @AppStorage("appInfo.selectedPane") private var selectedPane: Pane = .info + @AppStorage(AppInfoView.selectedPaneStorageKey) private var selectedPane: Pane = .info @State private var showPanicConfirmation = false @AppStorage(AppLanguageSettings.overrideKey) private var languageOverride = "" /// The override changed this session; localization resolves at process /// start, so surface the restart hint. @State private var showLanguageRestartNote = false - private enum Pane: String { + enum Pane: String { case settings case info } + private static let selectedPaneStorageKey = "appInfo.selectedPane" + + /// Overwrites the remembered pane so the next presentation opens there. + /// `selectedPane` is sticky by design; a caller whose control names a + /// specific pane (the header gear says "settings") uses this so the + /// sheet doesn't land first-time users on the Info default instead. + static func setSelectedPane(_ pane: Pane) { + UserDefaults.standard.set(pane.rawValue, forKey: selectedPaneStorageKey) + } + private var selectedTheme: AppTheme { AppTheme(rawValue: appThemeRawValue) ?? .matrix } diff --git a/bitchat/Views/ContentHeaderView.swift b/bitchat/Views/ContentHeaderView.swift index f857ba38..1e465be7 100644 --- a/bitchat/Views/ContentHeaderView.swift +++ b/bitchat/Views/ContentHeaderView.swift @@ -65,9 +65,10 @@ struct ContentHeaderView: View { // (next to the failed-wipe banner), not on this Text: a host // that can be covered or removed could take the pending // dialog down with it. - // This is the only entry point to App Info, but it reads as - // static text; surface the tap. (The triple-tap panic wipe - // stays undiscoverable on purpose — it's destructive.) + // The gear in the icon cluster is the discoverable entry + // point to App Info; the logo tap stays as a shortcut but + // reads as static text, so surface it. (The triple-tap panic + // wipe stays undiscoverable on purpose — it's destructive.) .accessibilityAddTraits(.isButton) .accessibilityHint( String(localized: "content.accessibility.app_info_hint", comment: "Accessibility hint on the bitchat/ logo explaining a tap opens app info") @@ -309,6 +310,26 @@ struct ContentHeaderView: View { ? String(localized: "content.accessibility.peers_connected", comment: "Accessibility value when peers are reachable") : String(localized: "content.accessibility.peers_none", comment: "Accessibility value when no peers are reachable")) ) + + // Settings entry point new users can actually find; the + // logo tap remains as a shortcut for those who know it. + // Opens on the settings pane explicitly: the sheet's pane + // memory defaults to info, and a button labeled "settings" + // must not land there. + Button(action: { appChromeModel.presentAppInfo(pane: .settings) }) { + Image(systemName: "gearshape") + .font(.bitchatSystem(size: 12)) + .foregroundColor(palette.secondary.opacity(0.9)) + .headerTapTarget() + } + .buttonStyle(.plain) + .padding(.leading, 4) + .accessibilityLabel( + String(localized: "content.header.settings", defaultValue: "settings", comment: "Accessibility label and tooltip for the header gear button that opens the settings/app info sheet") + ) + .help( + String(localized: "content.header.settings", defaultValue: "settings", comment: "Accessibility label and tooltip for the header gear button that opens the settings/app info sheet") + ) } .layoutPriority(3) .sheet(isPresented: $showVerifySheet) {