From 87f1561f342a1f33e5eb4e8f3035522ce3f3d6e3 Mon Sep 17 00:00:00 2001 From: ecgang Date: Mon, 6 Jul 2026 10:41:04 -0700 Subject: [PATCH] fix: move public trust caption to a localized caption band (#1064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ask 6 — SE-width header overflow: the mesh trust caption lived as a `.fixedSize(horizontal: true)` `Text` inside the header's trailing `.layoutPriority(3)` (non-compressible) cluster, so on a narrow (SE-width) header it forced the row to overflow. Relocated it out of the header into a persistent full-width caption band under the public timeline — the parity twin of the DM sheet's `privacyCaption` band (#1366): same structure (full width, `.themedSurface()`, centered, combined a11y), rendered above the composer in both the glass and matrix layouts, mesh-only (geohash/location channels carry no such caption). It is muted (`palette.secondary`), not orange, because orange is the DM privacy signal and this surface is deliberately public. `ContentView` gains `locationChannelsModel` (already injected on its environment in `BitchatApp`) to gate the mesh condition. Ask 7 — untranslatable hardcoded vocabulary: the caption and its accessibility label were `Text(verbatim:)` literals ("public · unencrypted" / "public, unencrypted") that bypassed `Localizable.xcstrings`. Replaced with two new localized keys — `content.header.public_caption` ("public · nearby", visible, `·` separator matching the DM caption band's "private · end-to-end encrypted") and `content.header.public_caption.a11y` ("public, nearby", VoiceOver, comma form). Both reuse the established public-surface vocabulary from #1357's `content.input.placeholder.mesh` ("… public, nearby") rather than coining a new "unencrypted" string. Only the `en` source value is seeded; other locales are left for translators, matching the existing DM-caption keys. NOT locally compiled (Swift/iOS; CI runs Build iOS + Swift Tests). Visual placement/spacing of the new band is unverified — needs an SE-width eyeball. Co-Authored-By: Claude Opus 4.8 (1M context) --- bitchat/Localizable.xcstrings | 24 ++++++++++++++++++++++++ bitchat/Views/ContentHeaderView.swift | 9 --------- bitchat/Views/ContentView.swift | 26 +++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index 7301e613..eb7cbd5b 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -18051,6 +18051,30 @@ } } }, + "content.header.public_caption" : { + "comment" : "Trust caption under the public mesh timeline: this channel is public and reaches nearby devices. Reuses the composer placeholder vocabulary (content.input.placeholder.mesh).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "public · nearby" + } + } + } + }, + "content.header.public_caption.a11y" : { + "comment" : "Accessibility label for the public mesh trust caption; comma form of content.header.public_caption for VoiceOver.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "public, nearby" + } + } + } + }, "content.help.verification" : { "extractionState" : "manual", "localizations" : { diff --git a/bitchat/Views/ContentHeaderView.swift b/bitchat/Views/ContentHeaderView.swift index fe027b67..eb188ed7 100644 --- a/bitchat/Views/ContentHeaderView.swift +++ b/bitchat/Views/ContentHeaderView.swift @@ -186,15 +186,6 @@ struct ContentHeaderView: View { } .buttonStyle(.plain) - if case .mesh = locationChannelsModel.selectedChannel { - Text(verbatim: "public · unencrypted") - .bitchatFont(size: 10) - .foregroundColor(palette.secondary) - .lineLimit(1) - .fixedSize(horizontal: true, vertical: false) - .accessibilityLabel(Text(verbatim: "public, unencrypted")) - } - Button(action: { withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) { showSidebar.toggle() diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 6d3c16dd..503a2bf7 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -35,6 +35,7 @@ struct ContentView: View { @EnvironmentObject private var privateConversationModel: PrivateConversationModel @EnvironmentObject private var verificationModel: VerificationModel @EnvironmentObject private var conversationUIModel: ConversationUIModel + @EnvironmentObject private var locationChannelsModel: LocationChannelsModel @StateObject private var voiceRecordingVM = VoiceRecordingViewModel() @State private var messageText = "" @@ -239,7 +240,10 @@ struct ContentView: View { } .safeAreaInset(edge: .bottom, spacing: 0) { if selectedPrivatePeerID == nil { - composerView + VStack(spacing: 0) { + meshPrivacyCaption + composerView + } } } } else { @@ -260,12 +264,32 @@ struct ContentView: View { Divider() if selectedPrivatePeerID == nil { + meshPrivacyCaption composerView } } } } + /// Persistent trust caption under the PUBLIC mesh timeline — the parity + /// twin of the DM sheet's `privacyCaption` (#1366). Moved here out of the + /// header's non-compressible trailing cluster, where its `.fixedSize` text + /// overflowed narrow (SE-width) headers. Mesh-only: geohash/location + /// channels carry no such caption. Muted rather than orange — orange is the + /// DM privacy signal; this surface is deliberately public. + @ViewBuilder + private var meshPrivacyCaption: some View { + if case .mesh = locationChannelsModel.selectedChannel { + Text("content.header.public_caption") + .bitchatFont(size: 11, weight: .medium) + .foregroundColor(palette.secondary) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + .themedSurface() + .accessibilityLabel(Text("content.header.public_caption.a11y")) + } + } + private var headerView: some View { ContentHeaderView( showSidebar: $appChromeModel.showSidebar,