mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 21:45:20 +00:00
ux-fixes: lock alignment, caption band, wrapping, tap targets, VoiceOver, theme sweep (#1366)
* Fix lock glyph alignment, privacy-caption band, and empty-state wrapping - Message-row locks: align to first text baseline instead of a hardcoded top padding that left the lock ~4pt below the line's visual center - Header/caption locks: 1pt optical lift (lock.fill ink is bottom-heavy; geometric centering reads low); seal badge stays untouched - DM privacy caption: sit on the themed surface like the rest of the bottom chrome instead of painting its own orange band - Empty-state lines: non-breaking spaces so the closing * can't orphan and 'bitchat/ for help' can't break right after the slash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Unify sheet close buttons, widen tiny tap targets, handle long nicknames - New SheetCloseButton component: one glyph size/weight (13 semibold), 32pt visual box, 44pt hit target; adopted by all 7 sheets (sizes had drifted across 12/13/14pt, two had no frame at all) - Favorite star buttons get real tap targets (peer list + DM header) - DM header nickname: single line with middle truncation instead of wrapping into the fixed-height header; peer-list names truncate tail - Geohash people rows: leading glyph 12 -> 10 to match mesh rows - Sidebar lock glyphs get the same optical lift as the DM header Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make channel switching, voice notes, and header actions work under VoiceOver - Channel rows in the location sheet are now single activatable buttons (label + selected trait + switch hint) with the bookmark toggle mirrored as a named accessibility action; bookmark buttons labeled - Voice-note mic: press-and-hold drag gestures can't be activated by VoiceOver, so the default accessibility action now toggles start/stop-and-send; announces 'recording' state; localized labels - Attachment button: camera (long-press) path exposed as a named action; labels localized instead of hardcoded English - People-count button announces connected vs no-one-reachable (was color-only); verification QR button gains a spoken name (.help is only a hint on iOS); bitchat/ logo exposes its tap-for-app-info as a button (panic triple-tap stays undiscoverable on purpose) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Theme-correctness sweep: palette colors everywhere, AX-size header growth - Fingerprint/verification sheet cards: palette-tinted boxes instead of fixed gray bands that ignored matrix green and occluded glass - Voice-note card: palette background (translucent) instead of opaque white/black; waveform + payment chips + image placeholder follow suit - .secondary/.primary/Color.blue swapped for palette.secondary/primary/ accentBlue across location sheets, people sheets, message captions, and the header count (system gray read wrong under matrix green) - Autocomplete/command rows: dropped the uniform gray wash that dulled the themed overlay panel - 'tap to reveal' caption follows the theme font instead of hardcoding monospaced - Headers use minHeight so two-line accessibility text sizes grow the bar instead of clipping inside it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix main header expanding to fill the screen The header bar's fixed height was load-bearing: its children fill the bar with .frame(maxHeight: .infinity) tap targets, so switching to an open-ended minHeight let the header expand to swallow all available vertical space, centering the title mid-screen and crushing the timeline into the composer. Restore the fixed height — headerHeight is a @ScaledMetric, so it already grows with Dynamic Type. Reproduced and verified both layouts with an offscreen render harness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * DM header: floating glass panel instead of muddy orange wash under glass Orange at 14% over the backdrop gradient reads as a gray-beige band, not a privacy signature. Under liquid glass the DM header now uses the same floating chrome panel as the main header; the private signature is already carried by the orange lock, caption, and composer accents. Matrix keeps its orange wash over the opaque themed surface, unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
8296630cf3
commit
d285c6ad53
@@ -8572,6 +8572,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.app_info_hint" : {
|
||||
"comment" : "Accessibility hint on the bitchat/ logo explaining a tap opens app info",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "shows app info"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.attach_photo" : {
|
||||
"comment" : "Accessibility label for the photo attachment button",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "attach photo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.attach_photo_hint" : {
|
||||
"comment" : "Accessibility hint explaining the attachment button opens the photo library",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "opens the photo library; use the take photo action for the camera"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.available_nostr" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -8930,6 +8966,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.choose_photo" : {
|
||||
"comment" : "Accessibility label for the macOS photo picker button",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "choose photo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.connected_mesh" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -9849,6 +9897,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.peers_connected" : {
|
||||
"comment" : "Accessibility value when peers are reachable",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "connected"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.peers_none" : {
|
||||
"comment" : "Accessibility value when no peers are reachable",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "no one reachable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.people_count" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -10770,6 +10842,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.record_voice_hint" : {
|
||||
"comment" : "Accessibility hint explaining double-tap toggles voice recording",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "double-tap to start recording, double-tap again to send"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.record_voice_note" : {
|
||||
"comment" : "Accessibility label for the voice note button",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "record voice note"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.recording" : {
|
||||
"comment" : "Accessibility value announced while a voice note is recording",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "recording"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.remove_favorite" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -11486,6 +11594,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.take_photo" : {
|
||||
"comment" : "Accessibility action name for taking a photo with the camera",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "take photo with camera"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.toggle_bookmark" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -11844,6 +11964,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.verification" : {
|
||||
"comment" : "Accessibility label for the verification QR button",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "verify encryption"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"content.accessibility.view_fingerprint_hint" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -17735,7 +17867,7 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "tap the channel name above to switch · tap bitchat/ for help"
|
||||
"value" : "tap the channel name above to switch · tap bitchat/ for help"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24292,6 +24424,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"location_channels.accessibility.add_bookmark" : {
|
||||
"comment" : "Accessibility action name for bookmarking a channel",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "bookmark channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"location_channels.accessibility.remove_bookmark" : {
|
||||
"comment" : "Accessibility action name for removing a channel bookmark",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "remove bookmark"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"location_channels.accessibility.switch_hint" : {
|
||||
"comment" : "Accessibility hint on a channel row explaining activation switches to it",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "switches to this channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"location_channels.action.open_settings" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
@@ -138,14 +138,8 @@ struct AppInfoView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("app_info.close")
|
||||
SheetCloseButton { dismiss() }
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ struct CommandSuggestionsView: View {
|
||||
buttonRow(for: command)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
}
|
||||
}
|
||||
.themedOverlayPanel()
|
||||
|
||||
@@ -57,7 +57,7 @@ struct PaymentChipView: View {
|
||||
|
||||
private var fgColor: Color { palette.primary }
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark ? Color.gray.opacity(0.18) : Color.gray.opacity(0.12)
|
||||
palette.secondary.opacity(colorScheme == .dark ? 0.18 : 0.12)
|
||||
}
|
||||
private var border: Color { fgColor.opacity(0.25) }
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// SheetCloseButton.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// The close "X" every sheet and header shares. One glyph size and weight
|
||||
/// everywhere (the sheets had drifted across 12/13/14pt), a 32pt visual box
|
||||
/// so existing header metrics don't move, and a hit target extended to 44pt
|
||||
/// per platform guidelines. Tint comes from the environment, so callers keep
|
||||
/// their own foreground color.
|
||||
struct SheetCloseButton: View {
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
.frame(width: 32, height: 32)
|
||||
.contentShape(Rectangle().inset(by: -6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(String(localized: "common.close", comment: "Accessibility label for close buttons"))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import BitFoundation
|
||||
struct TextMessageView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
@Environment(\.appTheme) private var theme
|
||||
@ThemedPalette private var palette
|
||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||
|
||||
let message: BitchatMessage
|
||||
@@ -36,14 +37,16 @@ struct TextMessageView: View {
|
||||
// Precompute heavy token scans once per row
|
||||
let cashuLinks = message.content.extractCashuLinks()
|
||||
let lightningLinks = message.content.extractLightningLinks()
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// Baseline alignment keeps the lock and delivery glyphs on the
|
||||
// first text line; a fixed top padding left the lock's solid body
|
||||
// hanging below the line's visual center.
|
||||
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
||||
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty
|
||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||
if message.isPrivate {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.bitchatSystem(size: 8))
|
||||
.foregroundColor(Color.orange.opacity(0.75))
|
||||
.padding(.top, 5)
|
||||
.padding(.trailing, 4)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
@@ -84,7 +87,7 @@ struct TextMessageView: View {
|
||||
} else if showDeliveryDetail {
|
||||
Text(verbatim: status.bitchatDescription)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
@@ -99,7 +102,7 @@ struct TextMessageView: View {
|
||||
else { expandedMessageIDs.insert(message.id) }
|
||||
}
|
||||
.bitchatFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.blue)
|
||||
.foregroundColor(palette.accentBlue)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ struct ContentComposerView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
}
|
||||
}
|
||||
.themedOverlayPanel()
|
||||
@@ -184,7 +183,19 @@ private extension ContentComposerView {
|
||||
imagePickerSourceType = .camera
|
||||
showImagePicker = true
|
||||
}
|
||||
.accessibilityLabel("Tap for library, long press for camera")
|
||||
.accessibilityLabel(
|
||||
String(localized: "content.accessibility.attach_photo", comment: "Accessibility label for the photo attachment button")
|
||||
)
|
||||
.accessibilityHint(
|
||||
String(localized: "content.accessibility.attach_photo_hint", comment: "Accessibility hint explaining the attachment button opens the photo library")
|
||||
)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
// The long-press → camera path is unreachable for VoiceOver users;
|
||||
// mirror it as a named action.
|
||||
.accessibilityAction(named: Text("content.accessibility.take_photo", comment: "Accessibility action name for taking a photo with the camera")) {
|
||||
imagePickerSourceType = .camera
|
||||
showImagePicker = true
|
||||
}
|
||||
#else
|
||||
Button(action: { showMacImagePicker = true }) {
|
||||
Image(systemName: "photo.circle.fill")
|
||||
@@ -193,7 +204,9 @@ private extension ContentComposerView {
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Choose photo")
|
||||
.accessibilityLabel(
|
||||
String(localized: "content.accessibility.choose_photo", comment: "Accessibility label for the macOS photo picker button")
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -235,7 +248,27 @@ private extension ContentComposerView {
|
||||
}
|
||||
)
|
||||
)
|
||||
.accessibilityLabel("Hold to record a voice note")
|
||||
.accessibilityLabel(
|
||||
String(localized: "content.accessibility.record_voice_note", comment: "Accessibility label for the voice note button")
|
||||
)
|
||||
.accessibilityValue(
|
||||
voiceRecordingVM.state.isActive
|
||||
? String(localized: "content.accessibility.recording", comment: "Accessibility value announced while a voice note is recording")
|
||||
: ""
|
||||
)
|
||||
.accessibilityHint(
|
||||
String(localized: "content.accessibility.record_voice_hint", comment: "Accessibility hint explaining double-tap toggles voice recording")
|
||||
)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
// Press-and-hold drag gestures can't be activated by VoiceOver;
|
||||
// give it a start/stop toggle as the default action.
|
||||
.accessibilityAction {
|
||||
if voiceRecordingVM.state.isActive {
|
||||
voiceRecordingVM.finish(completion: conversationUIModel.sendVoiceNote)
|
||||
} else {
|
||||
voiceRecordingVM.start(shouldShow: conversationUIModel.canSendMediaInCurrentContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendButtonView(enabled: Bool) -> some View {
|
||||
|
||||
@@ -33,6 +33,16 @@ struct ContentHeaderView: View {
|
||||
.onTapGesture(count: 1) {
|
||||
appChromeModel.presentAppInfo()
|
||||
}
|
||||
// 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.)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityHint(
|
||||
String(localized: "content.accessibility.app_info_hint", comment: "Accessibility hint on the bitchat/ logo explaining a tap opens app info")
|
||||
)
|
||||
.accessibilityAction {
|
||||
appChromeModel.presentAppInfo()
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text(verbatim: "@")
|
||||
@@ -183,6 +193,13 @@ struct ContentHeaderView: View {
|
||||
headerOtherPeersCount
|
||||
)
|
||||
)
|
||||
// Connected-vs-nobody is otherwise encoded only in the icon's
|
||||
// color; say it.
|
||||
.accessibilityValue(
|
||||
headerPeersReachable
|
||||
? 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")
|
||||
)
|
||||
}
|
||||
.layoutPriority(3)
|
||||
.sheet(isPresented: $showVerifySheet) {
|
||||
@@ -190,6 +207,11 @@ struct ContentHeaderView: View {
|
||||
.environmentObject(verificationModel)
|
||||
}
|
||||
}
|
||||
// Fixed height is load-bearing: children fill the bar with
|
||||
// .frame(maxHeight: .infinity) tap targets, so an open-ended
|
||||
// minHeight lets the header expand to swallow the whole screen.
|
||||
// headerHeight is a @ScaledMetric, so it still grows with Dynamic
|
||||
// Type.
|
||||
.frame(height: headerHeight)
|
||||
.padding(.horizontal, 12)
|
||||
.sheet(isPresented: $appChromeModel.isLocationChannelsSheetPresented) {
|
||||
@@ -266,14 +288,25 @@ private extension ContentHeaderView {
|
||||
dynamicTypeSize.isAccessibilitySize ? 2 : 1
|
||||
}
|
||||
|
||||
/// Whether anyone is actually reachable on the current channel — the
|
||||
/// state the count icon's color encodes visually.
|
||||
var headerPeersReachable: Bool {
|
||||
switch locationChannelsModel.selectedChannel {
|
||||
case .location:
|
||||
return peerListModel.visibleGeohashPeerCount > 0
|
||||
case .mesh:
|
||||
return peerListModel.connectedMeshPeerCount > 0
|
||||
}
|
||||
}
|
||||
|
||||
func channelPeopleCountAndColor() -> (Int, Color) {
|
||||
switch locationChannelsModel.selectedChannel {
|
||||
case .location:
|
||||
let count = peerListModel.visibleGeohashPeerCount
|
||||
return (count, count > 0 ? palette.locationAccent : Color.secondary)
|
||||
return (count, count > 0 ? palette.locationAccent : palette.secondary)
|
||||
case .mesh:
|
||||
let meshBlue = Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
|
||||
let color: Color = peerListModel.connectedMeshPeerCount > 0 ? meshBlue : Color.secondary
|
||||
let color: Color = peerListModel.connectedMeshPeerCount > 0 ? meshBlue : palette.secondary
|
||||
return (peerListModel.reachableMeshPeerCount, color)
|
||||
}
|
||||
}
|
||||
@@ -293,16 +326,10 @@ private struct ContentLocationNotesUnavailableView: View {
|
||||
Text("content.notes.title")
|
||||
.bitchatFont(size: 16, weight: .bold)
|
||||
Spacer()
|
||||
Button(action: { showLocationNotes = false }) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
.foregroundColor(palette.primary)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(String(localized: "common.close", comment: "Accessibility label for close buttons"))
|
||||
SheetCloseButton { showLocationNotes = false }
|
||||
.foregroundColor(palette.primary)
|
||||
}
|
||||
.frame(height: headerHeight)
|
||||
.frame(minHeight: headerHeight)
|
||||
.padding(.horizontal, 12)
|
||||
.themedChromePanel(edge: .top)
|
||||
Text("content.notes.location_unavailable")
|
||||
|
||||
@@ -160,24 +160,23 @@ private struct ContentPeopleListView: View {
|
||||
.font(.bitchatSystem(size: 14))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
// .help maps to the accessibility *hint* on iOS, so the
|
||||
// button still needs a spoken name.
|
||||
.accessibilityLabel(
|
||||
String(localized: "content.accessibility.verification", comment: "Accessibility label for the verification QR button")
|
||||
)
|
||||
.help(
|
||||
String(localized: "content.help.verification", comment: "Help text for verification button")
|
||||
)
|
||||
}
|
||||
Button(action: {
|
||||
SheetCloseButton {
|
||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||
dismiss()
|
||||
showSidebar = false
|
||||
showVerifySheet = false
|
||||
privateConversationModel.endConversation()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 12, weight: .semibold)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Close")
|
||||
}
|
||||
|
||||
let activeText = String.localizedStringWithFormat(
|
||||
@@ -199,13 +198,13 @@ private struct ContentPeopleListView: View {
|
||||
Text(subtitle)
|
||||
.foregroundColor(subtitleColor)
|
||||
Text(activeText)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
.bitchatFont(size: 12)
|
||||
} else {
|
||||
Text(activeText)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -341,6 +340,9 @@ private struct ContentPrivateChatSheetView: View {
|
||||
Image(systemName: headerState.isFavorite ? "star.fill" : "star")
|
||||
.font(.bitchatSystem(size: 14))
|
||||
.foregroundColor(headerState.isFavorite ? Color.yellow : palette.primary)
|
||||
// Same visual box + 44pt hit target as SheetCloseButton.
|
||||
.frame(width: 32, height: 32)
|
||||
.contentShape(Rectangle().inset(by: -6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(
|
||||
@@ -354,30 +356,20 @@ private struct ContentPrivateChatSheetView: View {
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button(action: {
|
||||
SheetCloseButton {
|
||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||
privateConversationModel.endConversation()
|
||||
showSidebar = true
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 12, weight: .semibold)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Close")
|
||||
}
|
||||
.frame(height: headerHeight)
|
||||
// minHeight so scaled text at accessibility sizes grows the
|
||||
// bar instead of clipping inside it.
|
||||
.frame(minHeight: headerHeight)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 12)
|
||||
// Orange tint before themedSurface so it layers in front of the
|
||||
// (opaque, in matrix) themed background rather than behind it.
|
||||
// Glass has no opaque backing — themedSurface is a no-op there,
|
||||
// so the wash needs more opacity to read over the backdrop
|
||||
// gradient's blue/purple glows.
|
||||
.background(Color.orange.opacity(theme.usesGlassChrome ? 0.14 : 0.06))
|
||||
.themedSurface()
|
||||
.modifier(PrivateHeaderChrome())
|
||||
}
|
||||
|
||||
MessageListView(
|
||||
@@ -446,13 +438,19 @@ private struct ContentPrivateChatSheetView: View {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.bitchatSystem(size: 9))
|
||||
// Optical centering: lock.fill's ink is bottom-heavy, so
|
||||
// geometric centering reads low next to the caption text.
|
||||
.offset(y: -1)
|
||||
Text(verbatim: privacyCaptionText)
|
||||
.bitchatFont(size: 11, weight: .medium)
|
||||
}
|
||||
.foregroundColor(Color.orange)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.orange.opacity(0.08))
|
||||
// The orange text is signature enough; a tinted band here reads as a
|
||||
// stray strip against the untinted composer chrome below it, so the
|
||||
// caption sits on the same surface as the rest of the bottom chrome.
|
||||
.themedSurface()
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
@@ -474,6 +472,28 @@ private struct ContentPrivateChatSheetView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Chrome for the private-chat header. Matrix keeps its orange privacy wash
|
||||
/// over an opaque themed surface. Glass gets the same floating panel as the
|
||||
/// main header instead: an orange wash over the backdrop gradient reads as a
|
||||
/// muddy gray-beige band, and the DM signature is already carried by the
|
||||
/// orange lock, caption, and composer accents.
|
||||
private struct PrivateHeaderChrome: ViewModifier {
|
||||
@Environment(\.appTheme) private var theme
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if theme.usesGlassChrome {
|
||||
content.themedChromePanel(edge: .top)
|
||||
} else {
|
||||
// Orange tint before themedSurface so it layers in front of the
|
||||
// opaque themed background rather than behind it.
|
||||
content
|
||||
.background(Color.orange.opacity(0.06))
|
||||
.themedSurface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ContentPrivateHeaderInfoButton: View {
|
||||
@EnvironmentObject private var appChromeModel: AppChromeModel
|
||||
@ThemedPalette private var palette
|
||||
@@ -506,17 +526,27 @@ private struct ContentPrivateHeaderInfoButton: View {
|
||||
// Absence of a glyph was the only offline signal; say it.
|
||||
Text("mesh_peers.state.offline")
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
|
||||
Text(headerState.displayName)
|
||||
.bitchatFont(size: 16, weight: .medium)
|
||||
.foregroundColor(palette.primary)
|
||||
// Middle truncation keeps the identity suffix visible on
|
||||
// long nicknames instead of wrapping into the fixed-height
|
||||
// header.
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
|
||||
if let encryptionStatus = headerState.encryptionStatus,
|
||||
let icon = encryptionStatus.icon {
|
||||
Image(systemName: icon)
|
||||
.font(.bitchatSystem(size: 14))
|
||||
// Optical centering: the lock glyphs' ink is bottom-heavy
|
||||
// (solid body, thin shackle), so geometric centering reads
|
||||
// ~1pt low next to the name. The seal badge is symmetric
|
||||
// and needs no lift.
|
||||
.offset(y: icon.hasPrefix("lock") ? -1 : 0)
|
||||
.foregroundColor(
|
||||
encryptionStatus == .noiseVerified || encryptionStatus == .noiseSecured
|
||||
? palette.primary
|
||||
@@ -543,6 +573,6 @@ private struct ContentPrivateHeaderInfoButton: View {
|
||||
.accessibilityHint(
|
||||
String(localized: "content.accessibility.view_fingerprint_hint", comment: "Accessibility hint for viewing encryption fingerprint")
|
||||
)
|
||||
.frame(height: headerHeight)
|
||||
.frame(minHeight: headerHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,8 @@ struct FingerprintView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.bitchatSystem(size: 14, weight: .semibold))
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
SheetCloseButton { dismiss() }
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -83,7 +80,7 @@ struct FingerprintView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.background(palette.secondary.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
|
||||
// Their fingerprint
|
||||
@@ -101,7 +98,7 @@ struct FingerprintView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.background(palette.secondary.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.contextMenu {
|
||||
Button(Strings.copy) {
|
||||
@@ -135,7 +132,7 @@ struct FingerprintView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.background(palette.secondary.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
.contextMenu {
|
||||
Button(Strings.copy) {
|
||||
|
||||
@@ -54,7 +54,9 @@ struct GeohashPeopleList: View {
|
||||
let assignedColor = peerListModel.colorForGeohashPerson(id: person.id, isDark: colorScheme == .dark)
|
||||
let rowColor: Color = person.isMe ? .orange : assignedColor
|
||||
Image(systemName: icon)
|
||||
.font(.bitchatSystem(size: 12))
|
||||
// Size 10 to match the mesh rows' leading glyphs —
|
||||
// both lists share the sidebar.
|
||||
.font(.bitchatSystem(size: 10))
|
||||
.foregroundColor(rowColor)
|
||||
.help(person.isTeleported ? Strings.teleported : Strings.nearby)
|
||||
|
||||
@@ -64,6 +66,8 @@ struct GeohashPeopleList: View {
|
||||
.bitchatFont(size: 14)
|
||||
.fontWeight(person.isMe ? .bold : .regular)
|
||||
.foregroundColor(rowColor)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
if !suffix.isEmpty {
|
||||
let suffixColor = person.isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
|
||||
Text(suffix)
|
||||
|
||||
@@ -31,6 +31,9 @@ struct LocationChannelsSheet: View {
|
||||
static let toggleOff: LocalizedStringKey = "common.toggle.off"
|
||||
|
||||
static let invalidGeohash = String(localized: "location_channels.error.invalid_geohash", comment: "Error shown when a custom geohash is invalid")
|
||||
static let switchChannelHint = String(localized: "location_channels.accessibility.switch_hint", comment: "Accessibility hint on a channel row explaining activation switches to it")
|
||||
static let addBookmark = String(localized: "location_channels.accessibility.add_bookmark", comment: "Accessibility action name for bookmarking a channel")
|
||||
static let removeBookmark = String(localized: "location_channels.accessibility.remove_bookmark", comment: "Accessibility action name for removing a channel bookmark")
|
||||
|
||||
static func meshTitle(_ count: Int) -> String {
|
||||
let label = String(localized: "location_channels.mesh_label", comment: "Label for the mesh channel row")
|
||||
@@ -103,7 +106,7 @@ struct LocationChannelsSheet: View {
|
||||
}
|
||||
Text(Strings.description)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
|
||||
Group {
|
||||
switch locationChannelsModel.permissionState {
|
||||
@@ -122,7 +125,7 @@ struct LocationChannelsSheet: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(Strings.permissionDenied)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
Button(Strings.openSettings, action: SystemSettings.location.open)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -169,13 +172,7 @@ struct LocationChannelsSheet: View {
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(action: { isPresented = false }) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Close")
|
||||
SheetCloseButton { isPresented = false }
|
||||
}
|
||||
|
||||
private var channelList: some View {
|
||||
@@ -210,7 +207,10 @@ struct LocationChannelsSheet: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
.accessibilityLabel(locationChannelsModel.isBookmarked(channel.geohash) ? Strings.removeBookmark : Strings.addBookmark)
|
||||
},
|
||||
accessoryActionTitle: locationChannelsModel.isBookmarked(channel.geohash) ? Strings.removeBookmark : Strings.addBookmark,
|
||||
accessoryAction: { locationChannelsModel.toggleBookmark(channel.geohash) }
|
||||
) {
|
||||
locationChannelsModel.markTeleported(for: channel.geohash, false)
|
||||
locationChannelsModel.select(ChannelID.location(channel))
|
||||
@@ -277,7 +277,7 @@ struct LocationChannelsSheet: View {
|
||||
HStack(spacing: 2) {
|
||||
Text(verbatim: "#")
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
TextField("geohash", text: $customGeohash)
|
||||
#if os(iOS)
|
||||
.textInputAutocapitalization(.never)
|
||||
@@ -319,7 +319,7 @@ struct LocationChannelsSheet: View {
|
||||
.bitchatFont(size: 14)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 10)
|
||||
.background(Color.secondary.opacity(0.12))
|
||||
.background(palette.secondary.opacity(0.12))
|
||||
.cornerRadius(6)
|
||||
.opacity(isValid ? 1.0 : 0.4)
|
||||
.disabled(!isValid)
|
||||
@@ -336,7 +336,7 @@ struct LocationChannelsSheet: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(Strings.bookmarked)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(Array(entries.enumerated()), id: \.offset) { index, gh in
|
||||
let level = levelForLength(gh.count)
|
||||
@@ -357,7 +357,10 @@ struct LocationChannelsSheet: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
.accessibilityLabel(locationChannelsModel.isBookmarked(gh) ? Strings.removeBookmark : Strings.addBookmark)
|
||||
},
|
||||
accessoryActionTitle: locationChannelsModel.isBookmarked(gh) ? Strings.removeBookmark : Strings.addBookmark,
|
||||
accessoryAction: { locationChannelsModel.toggleBookmark(gh) }
|
||||
) {
|
||||
let inRegional = locationChannelsModel.availableChannels.contains { $0.geohash == gh }
|
||||
if !inRegional && !locationChannelsModel.availableChannels.isEmpty {
|
||||
@@ -399,6 +402,8 @@ struct LocationChannelsSheet: View {
|
||||
titleColor: Color? = nil,
|
||||
titleBold: Bool = false,
|
||||
@ViewBuilder trailingAccessory: () -> some View = { EmptyView() },
|
||||
accessoryActionTitle: String? = nil,
|
||||
accessoryAction: (() -> Void)? = nil,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
@@ -409,17 +414,17 @@ struct LocationChannelsSheet: View {
|
||||
Text(parts.base)
|
||||
.bitchatFont(size: 14)
|
||||
.fontWeight(titleBold ? .bold : .regular)
|
||||
.foregroundColor(titleColor ?? Color.primary)
|
||||
.foregroundColor(titleColor ?? palette.primary)
|
||||
if let count = parts.countSuffix, !count.isEmpty {
|
||||
Text(count)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
}
|
||||
let subtitleFull = Strings.subtitle(prefix: subtitlePrefix, name: subtitleName)
|
||||
Text(subtitleFull)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
@@ -434,6 +439,19 @@ struct LocationChannelsSheet: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: action)
|
||||
// The row is a plain HStack with a tap gesture, which VoiceOver reads
|
||||
// as disconnected static text. Expose it as one activatable button;
|
||||
// the visible bookmark accessory is mirrored as a named action.
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(Text(verbatim: "\(title), \(Strings.subtitle(prefix: subtitlePrefix, name: subtitleName))"))
|
||||
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : [.isButton])
|
||||
.accessibilityHint(Strings.switchChannelHint)
|
||||
.accessibilityAction(.default, action)
|
||||
.accessibilityActions {
|
||||
if let accessoryActionTitle, let accessoryAction {
|
||||
Button(accessoryActionTitle, action: accessoryAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Split a title like "#mesh [3 people]" into base and suffix "[3 people]"
|
||||
@@ -477,16 +495,16 @@ extension LocationChannelsSheet {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(Strings.torTitle)
|
||||
.bitchatFont(size: 12, weight: .semibold)
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(palette.primary)
|
||||
Text(Strings.torSubtitle)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
}
|
||||
.toggleStyle(IRCToggleStyle(accent: palette.accent, onLabel: Strings.toggleOn, offLabel: Strings.toggleOff))
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color.secondary.opacity(0.12))
|
||||
.background(palette.secondary.opacity(0.12))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ struct LocationNotesView: View {
|
||||
private var maxDraftLines: Int { dynamicTypeSize.isAccessibilitySize ? 5 : 3 }
|
||||
|
||||
private enum Strings {
|
||||
static let closeAccessibility = String(localized: "common.close", comment: "Accessibility label for close buttons")
|
||||
static let description: LocalizedStringKey = "location_notes.description"
|
||||
static let loadingRecent: LocalizedStringKey = "location_notes.loading_recent"
|
||||
static let relaysPaused: LocalizedStringKey = "location_notes.relays_paused"
|
||||
@@ -97,13 +96,7 @@ struct LocationNotesView: View {
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "xmark")
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Strings.closeAccessibility)
|
||||
SheetCloseButton { dismiss() }
|
||||
}
|
||||
|
||||
private var headerSection: some View {
|
||||
@@ -126,12 +119,12 @@ struct LocationNotesView: View {
|
||||
}
|
||||
Text(Strings.description)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if manager.state == .noRelays {
|
||||
Text(Strings.relaysPaused)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
@@ -180,7 +173,7 @@ struct LocationNotesView: View {
|
||||
if !ts.isEmpty {
|
||||
Text(ts)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
@@ -197,7 +190,7 @@ struct LocationNotesView: View {
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
Text(Strings.relaysRetryHint)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
Button(Strings.retry) { manager.refresh() }
|
||||
.bitchatFont(size: 12)
|
||||
.buttonStyle(.plain)
|
||||
@@ -210,7 +203,7 @@ struct LocationNotesView: View {
|
||||
ProgressView()
|
||||
Text(Strings.loadingNotes)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
@@ -222,7 +215,7 @@ struct LocationNotesView: View {
|
||||
.bitchatFont(size: 13, weight: .semibold)
|
||||
Text(Strings.emptySubtitle)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ private typealias PlatformImage = NSImage
|
||||
#endif
|
||||
|
||||
struct BlockRevealImageView: View {
|
||||
@ThemedPalette private var palette
|
||||
private let url: URL
|
||||
private let revealProgress: Double?
|
||||
private let isSending: Bool
|
||||
@@ -92,7 +93,9 @@ struct BlockRevealImageView: View {
|
||||
Image(systemName: "eye.slash.fill")
|
||||
.font(.bitchatSystem(size: 24, weight: .semibold))
|
||||
Text(verbatim: Strings.tapToReveal)
|
||||
.font(.bitchatSystem(size: 12, weight: .medium, design: .monospaced))
|
||||
// Themed: monospaced under matrix,
|
||||
// system under liquid glass.
|
||||
.bitchatFont(size: 12, weight: .medium)
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.85))
|
||||
)
|
||||
@@ -100,13 +103,13 @@ struct BlockRevealImageView: View {
|
||||
}
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.fill(palette.secondary.opacity(0.2))
|
||||
.frame(height: 200)
|
||||
.overlay {
|
||||
if loadFailed {
|
||||
Image(systemName: "photo")
|
||||
.font(.bitchatSystem(size: 24, weight: .semibold))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
|
||||
@@ -11,6 +11,7 @@ import BitFoundation
|
||||
struct MediaMessageView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.appTheme) private var theme
|
||||
@ThemedPalette private var palette
|
||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||
let message: BitchatMessage
|
||||
let media: BitchatMessage.Media
|
||||
@@ -36,12 +37,14 @@ struct MediaMessageView: View {
|
||||
let isFromMe = conversationUIModel.isMediaMessageFromCurrentUser(message)
|
||||
let cancelAction: (() -> Void)? = state.canCancel ? { conversationUIModel.cancelMediaSend(messageID: message.id) } : nil
|
||||
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// Baseline alignment (via the header text inside the VStack) keeps the
|
||||
// lock on the header line; a fixed top padding left its solid body
|
||||
// hanging below the line's visual center.
|
||||
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
||||
if message.isPrivate {
|
||||
Image(systemName: "lock.fill")
|
||||
.font(.bitchatSystem(size: 8))
|
||||
.foregroundColor(Color.orange.opacity(0.75))
|
||||
.padding(.top, 5)
|
||||
.padding(.trailing, 4)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
@@ -81,7 +84,7 @@ struct MediaMessageView: View {
|
||||
} else if showDeliveryDetail {
|
||||
Text(verbatim: status.bitchatDescription)
|
||||
.bitchatFont(size: 11)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ struct VoiceNoteView: View {
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .dark ? Color.black.opacity(0.6) : Color.white
|
||||
// Palette-based and slightly translucent so the card doesn't sit as
|
||||
// an opaque white/black box over the glass gradient.
|
||||
palette.background.opacity(colorScheme == .dark ? 0.6 : 0.7)
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
@@ -69,7 +71,7 @@ struct VoiceNoteView: View {
|
||||
|
||||
Text(playbackLabel)
|
||||
.bitchatFont(size: 13)
|
||||
.foregroundColor(Color.secondary)
|
||||
.foregroundColor(palette.secondary)
|
||||
|
||||
if let onCancel = onCancel, isSending {
|
||||
Button(action: onCancel) {
|
||||
|
||||
@@ -42,7 +42,7 @@ struct WaveformView: View {
|
||||
} else if let send = clampedSend, binPosition <= send {
|
||||
color = palette.accentBlue
|
||||
} else {
|
||||
color = Color.gray.opacity(0.35)
|
||||
color = palette.secondary.opacity(0.35)
|
||||
}
|
||||
context.fill(Path(rect), with: .color(color))
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ struct MeshPeerList: View {
|
||||
Text(base)
|
||||
.bitchatFont(size: 14)
|
||||
.foregroundColor(baseColor)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
if !suffix.isEmpty {
|
||||
let suffixColor = isMe ? Color.orange.opacity(0.6) : baseColor.opacity(0.6)
|
||||
Text(suffix)
|
||||
@@ -112,6 +114,10 @@ struct MeshPeerList: View {
|
||||
if let icon = peer.encryptionStatus.icon {
|
||||
Image(systemName: icon)
|
||||
.font(.bitchatSystem(size: 10))
|
||||
// Optical centering: lock glyph ink is
|
||||
// bottom-heavy, so geometric centering
|
||||
// reads low next to the name.
|
||||
.offset(y: icon.hasPrefix("lock") ? -0.5 : 0)
|
||||
.foregroundColor(baseColor)
|
||||
}
|
||||
} else {
|
||||
@@ -124,6 +130,7 @@ struct MeshPeerList: View {
|
||||
// Fallback to whatever status says (likely lock if we had a past session)
|
||||
Image(systemName: icon)
|
||||
.font(.bitchatSystem(size: 10))
|
||||
.offset(y: icon.hasPrefix("lock") ? -0.5 : 0)
|
||||
.foregroundColor(baseColor)
|
||||
}
|
||||
}
|
||||
@@ -144,6 +151,11 @@ struct MeshPeerList: View {
|
||||
Image(systemName: peer.isFavorite ? "star.fill" : "star")
|
||||
.font(.bitchatSystem(size: 12))
|
||||
.foregroundColor(peer.isFavorite ? .yellow : palette.secondary)
|
||||
// Widen the tap target beyond the bare glyph;
|
||||
// height stays row-bound so neighboring rows
|
||||
// keep their own taps.
|
||||
.frame(width: 36)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
@@ -297,7 +297,9 @@ private extension MessageListView {
|
||||
}
|
||||
|
||||
func emptyStateLine(_ text: String) -> some View {
|
||||
Text(verbatim: "* \(text) *")
|
||||
// Non-breaking space before the closing asterisk so a tight wrap
|
||||
// can't orphan a lone "*" onto its own line.
|
||||
Text(verbatim: "* \(text)\u{00A0}*")
|
||||
.bitchatFont(size: 13)
|
||||
.foregroundColor(palette.secondary.opacity(0.9))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -11,7 +11,10 @@ import AppKit
|
||||
struct MyQRView: View {
|
||||
let qrString: String
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
private var boxColor: Color { Color.gray.opacity(0.1) }
|
||||
@ThemedPalette private var palette
|
||||
// Palette-tinted so the box follows the theme (green under matrix)
|
||||
// instead of a fixed gray band over the glass gradient.
|
||||
private var boxColor: Color { palette.secondary.opacity(0.1) }
|
||||
|
||||
private enum Strings {
|
||||
static let title: LocalizedStringKey = "verification.my_qr.title"
|
||||
@@ -50,6 +53,7 @@ struct MyQRView: View {
|
||||
struct QRCodeImage: View {
|
||||
let data: String
|
||||
let size: CGFloat
|
||||
@ThemedPalette private var palette
|
||||
|
||||
private let context = CIContext()
|
||||
private let filter = CIFilter.qrCodeGenerator()
|
||||
@@ -65,12 +69,12 @@ struct QRCodeImage: View {
|
||||
.frame(width: size, height: size)
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
|
||||
.stroke(palette.secondary.opacity(0.5), lineWidth: 1)
|
||||
.frame(width: size, height: size)
|
||||
.overlay(
|
||||
Text(Strings.unavailable)
|
||||
.bitchatFont(size: 12)
|
||||
.foregroundColor(.gray)
|
||||
.foregroundColor(palette.secondary)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,6 +112,7 @@ struct ImageWrapper: View {
|
||||
/// Placeholder scanner UI; real camera scanning will be added later.
|
||||
struct QRScanView: View {
|
||||
@EnvironmentObject private var verificationModel: VerificationModel
|
||||
@ThemedPalette private var palette
|
||||
var isActive: Bool = true
|
||||
var onSuccess: (() -> Void)? = nil // Called when verification succeeds
|
||||
@State private var input = ""
|
||||
@@ -153,7 +158,7 @@ struct QRScanView: View {
|
||||
.bitchatFont(size: 14, weight: .medium)
|
||||
TextEditor(text: $input)
|
||||
.frame(height: 100)
|
||||
.border(Color.gray.opacity(0.4))
|
||||
.border(palette.secondary.opacity(0.4))
|
||||
Button(Strings.validate) {
|
||||
// Deduplicate: ignore if we just processed this exact QR
|
||||
guard input != lastValid else {
|
||||
@@ -285,7 +290,7 @@ struct VerificationSheetView: View {
|
||||
|
||||
private var backgroundColor: Color { palette.background }
|
||||
private var accentColor: Color { palette.accent }
|
||||
private var boxColor: Color { Color.gray.opacity(0.1) }
|
||||
private var boxColor: Color { palette.secondary.opacity(0.1) }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -295,15 +300,11 @@ struct VerificationSheetView: View {
|
||||
.bitchatFont(size: 14, weight: .bold)
|
||||
.foregroundColor(accentColor)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
SheetCloseButton {
|
||||
showingScanner = false
|
||||
isPresented = false
|
||||
}) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.bitchatSystem(size: 14, weight: .semibold))
|
||||
.foregroundColor(accentColor)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(accentColor)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
|
||||
Reference in New Issue
Block a user