mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +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" : {
|
"content.accessibility.available_nostr" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"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" : {
|
"content.accessibility.connected_mesh" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"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" : {
|
"content.accessibility.people_count" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"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" : {
|
"content.accessibility.remove_favorite" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"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" : {
|
"content.accessibility.toggle_bookmark" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"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" : {
|
"content.accessibility.view_fingerprint_hint" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -17735,7 +17867,7 @@
|
|||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"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" : {
|
"location_channels.action.open_settings" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -138,14 +138,8 @@ struct AppInfoView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(action: { dismiss() }) {
|
SheetCloseButton { dismiss() }
|
||||||
Image(systemName: "xmark")
|
.foregroundColor(textColor)
|
||||||
.bitchatFont(size: 13, weight: .semibold)
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.accessibilityLabel("app_info.close")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ struct CommandSuggestionsView: View {
|
|||||||
buttonRow(for: command)
|
buttonRow(for: command)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.background(Color.gray.opacity(0.1))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.themedOverlayPanel()
|
.themedOverlayPanel()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ struct PaymentChipView: View {
|
|||||||
|
|
||||||
private var fgColor: Color { palette.primary }
|
private var fgColor: Color { palette.primary }
|
||||||
private var bgColor: Color {
|
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) }
|
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 {
|
struct TextMessageView: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||||
@Environment(\.appTheme) private var theme
|
@Environment(\.appTheme) private var theme
|
||||||
|
@ThemedPalette private var palette
|
||||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||||
|
|
||||||
let message: BitchatMessage
|
let message: BitchatMessage
|
||||||
@@ -36,14 +37,16 @@ struct TextMessageView: View {
|
|||||||
// Precompute heavy token scans once per row
|
// Precompute heavy token scans once per row
|
||||||
let cashuLinks = message.content.extractCashuLinks()
|
let cashuLinks = message.content.extractCashuLinks()
|
||||||
let lightningLinks = message.content.extractLightningLinks()
|
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 isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty
|
||||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||||
if message.isPrivate {
|
if message.isPrivate {
|
||||||
Image(systemName: "lock.fill")
|
Image(systemName: "lock.fill")
|
||||||
.font(.bitchatSystem(size: 8))
|
.font(.bitchatSystem(size: 8))
|
||||||
.foregroundColor(Color.orange.opacity(0.75))
|
.foregroundColor(Color.orange.opacity(0.75))
|
||||||
.padding(.top, 5)
|
|
||||||
.padding(.trailing, 4)
|
.padding(.trailing, 4)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,7 @@ struct TextMessageView: View {
|
|||||||
} else if showDeliveryDetail {
|
} else if showDeliveryDetail {
|
||||||
Text(verbatim: status.bitchatDescription)
|
Text(verbatim: status.bitchatDescription)
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
@@ -99,7 +102,7 @@ struct TextMessageView: View {
|
|||||||
else { expandedMessageIDs.insert(message.id) }
|
else { expandedMessageIDs.insert(message.id) }
|
||||||
}
|
}
|
||||||
.bitchatFont(size: 11, weight: .medium)
|
.bitchatFont(size: 11, weight: .medium)
|
||||||
.foregroundColor(Color.blue)
|
.foregroundColor(palette.accentBlue)
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ struct ContentComposerView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.background(Color.gray.opacity(0.1))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.themedOverlayPanel()
|
.themedOverlayPanel()
|
||||||
@@ -184,7 +183,19 @@ private extension ContentComposerView {
|
|||||||
imagePickerSourceType = .camera
|
imagePickerSourceType = .camera
|
||||||
showImagePicker = true
|
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
|
#else
|
||||||
Button(action: { showMacImagePicker = true }) {
|
Button(action: { showMacImagePicker = true }) {
|
||||||
Image(systemName: "photo.circle.fill")
|
Image(systemName: "photo.circle.fill")
|
||||||
@@ -193,7 +204,9 @@ private extension ContentComposerView {
|
|||||||
.frame(width: 36, height: 36)
|
.frame(width: 36, height: 36)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityLabel("Choose photo")
|
.accessibilityLabel(
|
||||||
|
String(localized: "content.accessibility.choose_photo", comment: "Accessibility label for the macOS photo picker button")
|
||||||
|
)
|
||||||
#endif
|
#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 {
|
func sendButtonView(enabled: Bool) -> some View {
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ struct ContentHeaderView: View {
|
|||||||
.onTapGesture(count: 1) {
|
.onTapGesture(count: 1) {
|
||||||
appChromeModel.presentAppInfo()
|
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) {
|
HStack(spacing: 0) {
|
||||||
Text(verbatim: "@")
|
Text(verbatim: "@")
|
||||||
@@ -183,6 +193,13 @@ struct ContentHeaderView: View {
|
|||||||
headerOtherPeersCount
|
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)
|
.layoutPriority(3)
|
||||||
.sheet(isPresented: $showVerifySheet) {
|
.sheet(isPresented: $showVerifySheet) {
|
||||||
@@ -190,6 +207,11 @@ struct ContentHeaderView: View {
|
|||||||
.environmentObject(verificationModel)
|
.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)
|
.frame(height: headerHeight)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.sheet(isPresented: $appChromeModel.isLocationChannelsSheetPresented) {
|
.sheet(isPresented: $appChromeModel.isLocationChannelsSheetPresented) {
|
||||||
@@ -266,14 +288,25 @@ private extension ContentHeaderView {
|
|||||||
dynamicTypeSize.isAccessibilitySize ? 2 : 1
|
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) {
|
func channelPeopleCountAndColor() -> (Int, Color) {
|
||||||
switch locationChannelsModel.selectedChannel {
|
switch locationChannelsModel.selectedChannel {
|
||||||
case .location:
|
case .location:
|
||||||
let count = peerListModel.visibleGeohashPeerCount
|
let count = peerListModel.visibleGeohashPeerCount
|
||||||
return (count, count > 0 ? palette.locationAccent : Color.secondary)
|
return (count, count > 0 ? palette.locationAccent : palette.secondary)
|
||||||
case .mesh:
|
case .mesh:
|
||||||
let meshBlue = Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
|
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)
|
return (peerListModel.reachableMeshPeerCount, color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,16 +326,10 @@ private struct ContentLocationNotesUnavailableView: View {
|
|||||||
Text("content.notes.title")
|
Text("content.notes.title")
|
||||||
.bitchatFont(size: 16, weight: .bold)
|
.bitchatFont(size: 16, weight: .bold)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: { showLocationNotes = false }) {
|
SheetCloseButton { showLocationNotes = false }
|
||||||
Image(systemName: "xmark")
|
.foregroundColor(palette.primary)
|
||||||
.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"))
|
|
||||||
}
|
}
|
||||||
.frame(height: headerHeight)
|
.frame(minHeight: headerHeight)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.themedChromePanel(edge: .top)
|
.themedChromePanel(edge: .top)
|
||||||
Text("content.notes.location_unavailable")
|
Text("content.notes.location_unavailable")
|
||||||
|
|||||||
@@ -160,24 +160,23 @@ private struct ContentPeopleListView: View {
|
|||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.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(
|
.help(
|
||||||
String(localized: "content.help.verification", comment: "Help text for verification button")
|
String(localized: "content.help.verification", comment: "Help text for verification button")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(action: {
|
SheetCloseButton {
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||||
dismiss()
|
dismiss()
|
||||||
showSidebar = false
|
showSidebar = false
|
||||||
showVerifySheet = false
|
showVerifySheet = false
|
||||||
privateConversationModel.endConversation()
|
privateConversationModel.endConversation()
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.bitchatFont(size: 12, weight: .semibold)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
|
||||||
.accessibilityLabel("Close")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeText = String.localizedStringWithFormat(
|
let activeText = String.localizedStringWithFormat(
|
||||||
@@ -199,13 +198,13 @@ private struct ContentPeopleListView: View {
|
|||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.foregroundColor(subtitleColor)
|
.foregroundColor(subtitleColor)
|
||||||
Text(activeText)
|
Text(activeText)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
} else {
|
} else {
|
||||||
Text(activeText)
|
Text(activeText)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -341,6 +340,9 @@ private struct ContentPrivateChatSheetView: View {
|
|||||||
Image(systemName: headerState.isFavorite ? "star.fill" : "star")
|
Image(systemName: headerState.isFavorite ? "star.fill" : "star")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
.foregroundColor(headerState.isFavorite ? Color.yellow : palette.primary)
|
.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)
|
.buttonStyle(.plain)
|
||||||
.accessibilityLabel(
|
.accessibilityLabel(
|
||||||
@@ -354,30 +356,20 @@ private struct ContentPrivateChatSheetView: View {
|
|||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
Button(action: {
|
SheetCloseButton {
|
||||||
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
|
||||||
privateConversationModel.endConversation()
|
privateConversationModel.endConversation()
|
||||||
showSidebar = true
|
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(.horizontal, 16)
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
.padding(.bottom, 12)
|
.padding(.bottom, 12)
|
||||||
// Orange tint before themedSurface so it layers in front of the
|
.modifier(PrivateHeaderChrome())
|
||||||
// (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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageListView(
|
MessageListView(
|
||||||
@@ -446,13 +438,19 @@ private struct ContentPrivateChatSheetView: View {
|
|||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
Image(systemName: "lock.fill")
|
Image(systemName: "lock.fill")
|
||||||
.font(.bitchatSystem(size: 9))
|
.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)
|
Text(verbatim: privacyCaptionText)
|
||||||
.bitchatFont(size: 11, weight: .medium)
|
.bitchatFont(size: 11, weight: .medium)
|
||||||
}
|
}
|
||||||
.foregroundColor(Color.orange)
|
.foregroundColor(Color.orange)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 4)
|
.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)
|
.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 {
|
private struct ContentPrivateHeaderInfoButton: View {
|
||||||
@EnvironmentObject private var appChromeModel: AppChromeModel
|
@EnvironmentObject private var appChromeModel: AppChromeModel
|
||||||
@ThemedPalette private var palette
|
@ThemedPalette private var palette
|
||||||
@@ -506,17 +526,27 @@ private struct ContentPrivateHeaderInfoButton: View {
|
|||||||
// Absence of a glyph was the only offline signal; say it.
|
// Absence of a glyph was the only offline signal; say it.
|
||||||
Text("mesh_peers.state.offline")
|
Text("mesh_peers.state.offline")
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(headerState.displayName)
|
Text(headerState.displayName)
|
||||||
.bitchatFont(size: 16, weight: .medium)
|
.bitchatFont(size: 16, weight: .medium)
|
||||||
.foregroundColor(palette.primary)
|
.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,
|
if let encryptionStatus = headerState.encryptionStatus,
|
||||||
let icon = encryptionStatus.icon {
|
let icon = encryptionStatus.icon {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.bitchatSystem(size: 14))
|
.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(
|
.foregroundColor(
|
||||||
encryptionStatus == .noiseVerified || encryptionStatus == .noiseSecured
|
encryptionStatus == .noiseVerified || encryptionStatus == .noiseSecured
|
||||||
? palette.primary
|
? palette.primary
|
||||||
@@ -543,6 +573,6 @@ private struct ContentPrivateHeaderInfoButton: View {
|
|||||||
.accessibilityHint(
|
.accessibilityHint(
|
||||||
String(localized: "content.accessibility.view_fingerprint_hint", comment: "Accessibility hint for viewing encryption fingerprint")
|
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()
|
Spacer()
|
||||||
|
|
||||||
Button(action: { dismiss() }) {
|
SheetCloseButton { dismiss() }
|
||||||
Image(systemName: "xmark")
|
.foregroundColor(textColor)
|
||||||
.font(.bitchatSystem(size: 14, weight: .semibold))
|
|
||||||
}
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
@@ -83,7 +80,7 @@ struct FingerprintView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color.gray.opacity(0.1))
|
.background(palette.secondary.opacity(0.1))
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
|
|
||||||
// Their fingerprint
|
// Their fingerprint
|
||||||
@@ -101,7 +98,7 @@ struct FingerprintView: View {
|
|||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding()
|
.padding()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(Color.gray.opacity(0.1))
|
.background(palette.secondary.opacity(0.1))
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button(Strings.copy) {
|
Button(Strings.copy) {
|
||||||
@@ -135,7 +132,7 @@ struct FingerprintView: View {
|
|||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding()
|
.padding()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(Color.gray.opacity(0.1))
|
.background(palette.secondary.opacity(0.1))
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button(Strings.copy) {
|
Button(Strings.copy) {
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ struct GeohashPeopleList: View {
|
|||||||
let assignedColor = peerListModel.colorForGeohashPerson(id: person.id, isDark: colorScheme == .dark)
|
let assignedColor = peerListModel.colorForGeohashPerson(id: person.id, isDark: colorScheme == .dark)
|
||||||
let rowColor: Color = person.isMe ? .orange : assignedColor
|
let rowColor: Color = person.isMe ? .orange : assignedColor
|
||||||
Image(systemName: icon)
|
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)
|
.foregroundColor(rowColor)
|
||||||
.help(person.isTeleported ? Strings.teleported : Strings.nearby)
|
.help(person.isTeleported ? Strings.teleported : Strings.nearby)
|
||||||
|
|
||||||
@@ -64,6 +66,8 @@ struct GeohashPeopleList: View {
|
|||||||
.bitchatFont(size: 14)
|
.bitchatFont(size: 14)
|
||||||
.fontWeight(person.isMe ? .bold : .regular)
|
.fontWeight(person.isMe ? .bold : .regular)
|
||||||
.foregroundColor(rowColor)
|
.foregroundColor(rowColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
if !suffix.isEmpty {
|
if !suffix.isEmpty {
|
||||||
let suffixColor = person.isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
|
let suffixColor = person.isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
|
||||||
Text(suffix)
|
Text(suffix)
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ struct LocationChannelsSheet: View {
|
|||||||
static let toggleOff: LocalizedStringKey = "common.toggle.off"
|
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 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 {
|
static func meshTitle(_ count: Int) -> String {
|
||||||
let label = String(localized: "location_channels.mesh_label", comment: "Label for the mesh channel row")
|
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)
|
Text(Strings.description)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
switch locationChannelsModel.permissionState {
|
switch locationChannelsModel.permissionState {
|
||||||
@@ -122,7 +125,7 @@ struct LocationChannelsSheet: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(Strings.permissionDenied)
|
Text(Strings.permissionDenied)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
Button(Strings.openSettings, action: SystemSettings.location.open)
|
Button(Strings.openSettings, action: SystemSettings.location.open)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
@@ -169,13 +172,7 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var closeButton: some View {
|
private var closeButton: some View {
|
||||||
Button(action: { isPresented = false }) {
|
SheetCloseButton { isPresented = false }
|
||||||
Image(systemName: "xmark")
|
|
||||||
.bitchatFont(size: 13, weight: .semibold)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.accessibilityLabel("Close")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var channelList: some View {
|
private var channelList: some View {
|
||||||
@@ -210,7 +207,10 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(.leading, 8)
|
.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.markTeleported(for: channel.geohash, false)
|
||||||
locationChannelsModel.select(ChannelID.location(channel))
|
locationChannelsModel.select(ChannelID.location(channel))
|
||||||
@@ -277,7 +277,7 @@ struct LocationChannelsSheet: View {
|
|||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
Text(verbatim: "#")
|
Text(verbatim: "#")
|
||||||
.bitchatFont(size: 14)
|
.bitchatFont(size: 14)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
TextField("geohash", text: $customGeohash)
|
TextField("geohash", text: $customGeohash)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
@@ -319,7 +319,7 @@ struct LocationChannelsSheet: View {
|
|||||||
.bitchatFont(size: 14)
|
.bitchatFont(size: 14)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.background(Color.secondary.opacity(0.12))
|
.background(palette.secondary.opacity(0.12))
|
||||||
.cornerRadius(6)
|
.cornerRadius(6)
|
||||||
.opacity(isValid ? 1.0 : 0.4)
|
.opacity(isValid ? 1.0 : 0.4)
|
||||||
.disabled(!isValid)
|
.disabled(!isValid)
|
||||||
@@ -336,7 +336,7 @@ struct LocationChannelsSheet: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(Strings.bookmarked)
|
Text(Strings.bookmarked)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(Array(entries.enumerated()), id: \.offset) { index, gh in
|
ForEach(Array(entries.enumerated()), id: \.offset) { index, gh in
|
||||||
let level = levelForLength(gh.count)
|
let level = levelForLength(gh.count)
|
||||||
@@ -357,7 +357,10 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(.leading, 8)
|
.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 }
|
let inRegional = locationChannelsModel.availableChannels.contains { $0.geohash == gh }
|
||||||
if !inRegional && !locationChannelsModel.availableChannels.isEmpty {
|
if !inRegional && !locationChannelsModel.availableChannels.isEmpty {
|
||||||
@@ -399,6 +402,8 @@ struct LocationChannelsSheet: View {
|
|||||||
titleColor: Color? = nil,
|
titleColor: Color? = nil,
|
||||||
titleBold: Bool = false,
|
titleBold: Bool = false,
|
||||||
@ViewBuilder trailingAccessory: () -> some View = { EmptyView() },
|
@ViewBuilder trailingAccessory: () -> some View = { EmptyView() },
|
||||||
|
accessoryActionTitle: String? = nil,
|
||||||
|
accessoryAction: (() -> Void)? = nil,
|
||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) -> some View {
|
) -> some View {
|
||||||
HStack(alignment: .center, spacing: 8) {
|
HStack(alignment: .center, spacing: 8) {
|
||||||
@@ -409,17 +414,17 @@ struct LocationChannelsSheet: View {
|
|||||||
Text(parts.base)
|
Text(parts.base)
|
||||||
.bitchatFont(size: 14)
|
.bitchatFont(size: 14)
|
||||||
.fontWeight(titleBold ? .bold : .regular)
|
.fontWeight(titleBold ? .bold : .regular)
|
||||||
.foregroundColor(titleColor ?? Color.primary)
|
.foregroundColor(titleColor ?? palette.primary)
|
||||||
if let count = parts.countSuffix, !count.isEmpty {
|
if let count = parts.countSuffix, !count.isEmpty {
|
||||||
Text(count)
|
Text(count)
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let subtitleFull = Strings.subtitle(prefix: subtitlePrefix, name: subtitleName)
|
let subtitleFull = Strings.subtitle(prefix: subtitlePrefix, name: subtitleName)
|
||||||
Text(subtitleFull)
|
Text(subtitleFull)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
@@ -434,6 +439,19 @@ struct LocationChannelsSheet: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture(perform: action)
|
.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]"
|
// Split a title like "#mesh [3 people]" into base and suffix "[3 people]"
|
||||||
@@ -477,16 +495,16 @@ extension LocationChannelsSheet {
|
|||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(Strings.torTitle)
|
Text(Strings.torTitle)
|
||||||
.bitchatFont(size: 12, weight: .semibold)
|
.bitchatFont(size: 12, weight: .semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(palette.primary)
|
||||||
Text(Strings.torSubtitle)
|
Text(Strings.torSubtitle)
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toggleStyle(IRCToggleStyle(accent: palette.accent, onLabel: Strings.toggleOn, offLabel: Strings.toggleOff))
|
.toggleStyle(IRCToggleStyle(accent: palette.accent, onLabel: Strings.toggleOn, offLabel: Strings.toggleOff))
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(Color.secondary.opacity(0.12))
|
.background(palette.secondary.opacity(0.12))
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ struct LocationNotesView: View {
|
|||||||
private var maxDraftLines: Int { dynamicTypeSize.isAccessibilitySize ? 5 : 3 }
|
private var maxDraftLines: Int { dynamicTypeSize.isAccessibilitySize ? 5 : 3 }
|
||||||
|
|
||||||
private enum Strings {
|
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 description: LocalizedStringKey = "location_notes.description"
|
||||||
static let loadingRecent: LocalizedStringKey = "location_notes.loading_recent"
|
static let loadingRecent: LocalizedStringKey = "location_notes.loading_recent"
|
||||||
static let relaysPaused: LocalizedStringKey = "location_notes.relays_paused"
|
static let relaysPaused: LocalizedStringKey = "location_notes.relays_paused"
|
||||||
@@ -97,13 +96,7 @@ struct LocationNotesView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var closeButton: some View {
|
private var closeButton: some View {
|
||||||
Button(action: { dismiss() }) {
|
SheetCloseButton { dismiss() }
|
||||||
Image(systemName: "xmark")
|
|
||||||
.bitchatFont(size: 13, weight: .semibold)
|
|
||||||
.frame(width: 32, height: 32)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.accessibilityLabel(Strings.closeAccessibility)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var headerSection: some View {
|
private var headerSection: some View {
|
||||||
@@ -126,12 +119,12 @@ struct LocationNotesView: View {
|
|||||||
}
|
}
|
||||||
Text(Strings.description)
|
Text(Strings.description)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
if manager.state == .noRelays {
|
if manager.state == .noRelays {
|
||||||
Text(Strings.relaysPaused)
|
Text(Strings.relaysPaused)
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
@@ -180,7 +173,7 @@ struct LocationNotesView: View {
|
|||||||
if !ts.isEmpty {
|
if !ts.isEmpty {
|
||||||
Text(ts)
|
Text(ts)
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -197,7 +190,7 @@ struct LocationNotesView: View {
|
|||||||
.bitchatFont(size: 13, weight: .semibold)
|
.bitchatFont(size: 13, weight: .semibold)
|
||||||
Text(Strings.relaysRetryHint)
|
Text(Strings.relaysRetryHint)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
Button(Strings.retry) { manager.refresh() }
|
Button(Strings.retry) { manager.refresh() }
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -210,7 +203,7 @@ struct LocationNotesView: View {
|
|||||||
ProgressView()
|
ProgressView()
|
||||||
Text(Strings.loadingNotes)
|
Text(Strings.loadingNotes)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
@@ -222,7 +215,7 @@ struct LocationNotesView: View {
|
|||||||
.bitchatFont(size: 13, weight: .semibold)
|
.bitchatFont(size: 13, weight: .semibold)
|
||||||
Text(Strings.emptySubtitle)
|
Text(Strings.emptySubtitle)
|
||||||
.bitchatFont(size: 12)
|
.bitchatFont(size: 12)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ private typealias PlatformImage = NSImage
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct BlockRevealImageView: View {
|
struct BlockRevealImageView: View {
|
||||||
|
@ThemedPalette private var palette
|
||||||
private let url: URL
|
private let url: URL
|
||||||
private let revealProgress: Double?
|
private let revealProgress: Double?
|
||||||
private let isSending: Bool
|
private let isSending: Bool
|
||||||
@@ -92,7 +93,9 @@ struct BlockRevealImageView: View {
|
|||||||
Image(systemName: "eye.slash.fill")
|
Image(systemName: "eye.slash.fill")
|
||||||
.font(.bitchatSystem(size: 24, weight: .semibold))
|
.font(.bitchatSystem(size: 24, weight: .semibold))
|
||||||
Text(verbatim: Strings.tapToReveal)
|
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))
|
.foregroundColor(.white.opacity(0.85))
|
||||||
)
|
)
|
||||||
@@ -100,13 +103,13 @@ struct BlockRevealImageView: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(Color.gray.opacity(0.2))
|
.fill(palette.secondary.opacity(0.2))
|
||||||
.frame(height: 200)
|
.frame(height: 200)
|
||||||
.overlay {
|
.overlay {
|
||||||
if loadFailed {
|
if loadFailed {
|
||||||
Image(systemName: "photo")
|
Image(systemName: "photo")
|
||||||
.font(.bitchatSystem(size: 24, weight: .semibold))
|
.font(.bitchatSystem(size: 24, weight: .semibold))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
} else {
|
} else {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import BitFoundation
|
|||||||
struct MediaMessageView: View {
|
struct MediaMessageView: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@Environment(\.appTheme) private var theme
|
@Environment(\.appTheme) private var theme
|
||||||
|
@ThemedPalette private var palette
|
||||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||||
let message: BitchatMessage
|
let message: BitchatMessage
|
||||||
let media: BitchatMessage.Media
|
let media: BitchatMessage.Media
|
||||||
@@ -36,12 +37,14 @@ struct MediaMessageView: View {
|
|||||||
let isFromMe = conversationUIModel.isMediaMessageFromCurrentUser(message)
|
let isFromMe = conversationUIModel.isMediaMessageFromCurrentUser(message)
|
||||||
let cancelAction: (() -> Void)? = state.canCancel ? { conversationUIModel.cancelMediaSend(messageID: message.id) } : nil
|
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 {
|
if message.isPrivate {
|
||||||
Image(systemName: "lock.fill")
|
Image(systemName: "lock.fill")
|
||||||
.font(.bitchatSystem(size: 8))
|
.font(.bitchatSystem(size: 8))
|
||||||
.foregroundColor(Color.orange.opacity(0.75))
|
.foregroundColor(Color.orange.opacity(0.75))
|
||||||
.padding(.top, 5)
|
|
||||||
.padding(.trailing, 4)
|
.padding(.trailing, 4)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,7 @@ struct MediaMessageView: View {
|
|||||||
} else if showDeliveryDetail {
|
} else if showDeliveryDetail {
|
||||||
Text(verbatim: status.bitchatDescription)
|
Text(verbatim: status.bitchatDescription)
|
||||||
.bitchatFont(size: 11)
|
.bitchatFont(size: 11)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ struct VoiceNoteView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var backgroundColor: Color {
|
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 {
|
private var borderColor: Color {
|
||||||
@@ -69,7 +71,7 @@ struct VoiceNoteView: View {
|
|||||||
|
|
||||||
Text(playbackLabel)
|
Text(playbackLabel)
|
||||||
.bitchatFont(size: 13)
|
.bitchatFont(size: 13)
|
||||||
.foregroundColor(Color.secondary)
|
.foregroundColor(palette.secondary)
|
||||||
|
|
||||||
if let onCancel = onCancel, isSending {
|
if let onCancel = onCancel, isSending {
|
||||||
Button(action: onCancel) {
|
Button(action: onCancel) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ struct WaveformView: View {
|
|||||||
} else if let send = clampedSend, binPosition <= send {
|
} else if let send = clampedSend, binPosition <= send {
|
||||||
color = palette.accentBlue
|
color = palette.accentBlue
|
||||||
} else {
|
} else {
|
||||||
color = Color.gray.opacity(0.35)
|
color = palette.secondary.opacity(0.35)
|
||||||
}
|
}
|
||||||
context.fill(Path(rect), with: .color(color))
|
context.fill(Path(rect), with: .color(color))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ struct MeshPeerList: View {
|
|||||||
Text(base)
|
Text(base)
|
||||||
.bitchatFont(size: 14)
|
.bitchatFont(size: 14)
|
||||||
.foregroundColor(baseColor)
|
.foregroundColor(baseColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
if !suffix.isEmpty {
|
if !suffix.isEmpty {
|
||||||
let suffixColor = isMe ? Color.orange.opacity(0.6) : baseColor.opacity(0.6)
|
let suffixColor = isMe ? Color.orange.opacity(0.6) : baseColor.opacity(0.6)
|
||||||
Text(suffix)
|
Text(suffix)
|
||||||
@@ -112,6 +114,10 @@ struct MeshPeerList: View {
|
|||||||
if let icon = peer.encryptionStatus.icon {
|
if let icon = peer.encryptionStatus.icon {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.bitchatSystem(size: 10))
|
.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)
|
.foregroundColor(baseColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -124,6 +130,7 @@ struct MeshPeerList: View {
|
|||||||
// Fallback to whatever status says (likely lock if we had a past session)
|
// Fallback to whatever status says (likely lock if we had a past session)
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.bitchatSystem(size: 10))
|
.font(.bitchatSystem(size: 10))
|
||||||
|
.offset(y: icon.hasPrefix("lock") ? -0.5 : 0)
|
||||||
.foregroundColor(baseColor)
|
.foregroundColor(baseColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,6 +151,11 @@ struct MeshPeerList: View {
|
|||||||
Image(systemName: peer.isFavorite ? "star.fill" : "star")
|
Image(systemName: peer.isFavorite ? "star.fill" : "star")
|
||||||
.font(.bitchatSystem(size: 12))
|
.font(.bitchatSystem(size: 12))
|
||||||
.foregroundColor(peer.isFavorite ? .yellow : palette.secondary)
|
.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)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,9 @@ private extension MessageListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func emptyStateLine(_ text: String) -> some View {
|
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)
|
.bitchatFont(size: 13)
|
||||||
.foregroundColor(palette.secondary.opacity(0.9))
|
.foregroundColor(palette.secondary.opacity(0.9))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import AppKit
|
|||||||
struct MyQRView: View {
|
struct MyQRView: View {
|
||||||
let qrString: String
|
let qrString: String
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@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 {
|
private enum Strings {
|
||||||
static let title: LocalizedStringKey = "verification.my_qr.title"
|
static let title: LocalizedStringKey = "verification.my_qr.title"
|
||||||
@@ -50,6 +53,7 @@ struct MyQRView: View {
|
|||||||
struct QRCodeImage: View {
|
struct QRCodeImage: View {
|
||||||
let data: String
|
let data: String
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
@ThemedPalette private var palette
|
||||||
|
|
||||||
private let context = CIContext()
|
private let context = CIContext()
|
||||||
private let filter = CIFilter.qrCodeGenerator()
|
private let filter = CIFilter.qrCodeGenerator()
|
||||||
@@ -65,12 +69,12 @@ struct QRCodeImage: View {
|
|||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
|
.stroke(palette.secondary.opacity(0.5), lineWidth: 1)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.overlay(
|
.overlay(
|
||||||
Text(Strings.unavailable)
|
Text(Strings.unavailable)
|
||||||
.bitchatFont(size: 12)
|
.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.
|
/// Placeholder scanner UI; real camera scanning will be added later.
|
||||||
struct QRScanView: View {
|
struct QRScanView: View {
|
||||||
@EnvironmentObject private var verificationModel: VerificationModel
|
@EnvironmentObject private var verificationModel: VerificationModel
|
||||||
|
@ThemedPalette private var palette
|
||||||
var isActive: Bool = true
|
var isActive: Bool = true
|
||||||
var onSuccess: (() -> Void)? = nil // Called when verification succeeds
|
var onSuccess: (() -> Void)? = nil // Called when verification succeeds
|
||||||
@State private var input = ""
|
@State private var input = ""
|
||||||
@@ -153,7 +158,7 @@ struct QRScanView: View {
|
|||||||
.bitchatFont(size: 14, weight: .medium)
|
.bitchatFont(size: 14, weight: .medium)
|
||||||
TextEditor(text: $input)
|
TextEditor(text: $input)
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
.border(Color.gray.opacity(0.4))
|
.border(palette.secondary.opacity(0.4))
|
||||||
Button(Strings.validate) {
|
Button(Strings.validate) {
|
||||||
// Deduplicate: ignore if we just processed this exact QR
|
// Deduplicate: ignore if we just processed this exact QR
|
||||||
guard input != lastValid else {
|
guard input != lastValid else {
|
||||||
@@ -285,7 +290,7 @@ struct VerificationSheetView: View {
|
|||||||
|
|
||||||
private var backgroundColor: Color { palette.background }
|
private var backgroundColor: Color { palette.background }
|
||||||
private var accentColor: Color { palette.accent }
|
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 {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -295,15 +300,11 @@ struct VerificationSheetView: View {
|
|||||||
.bitchatFont(size: 14, weight: .bold)
|
.bitchatFont(size: 14, weight: .bold)
|
||||||
.foregroundColor(accentColor)
|
.foregroundColor(accentColor)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: {
|
SheetCloseButton {
|
||||||
showingScanner = false
|
showingScanner = false
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.font(.bitchatSystem(size: 14, weight: .semibold))
|
|
||||||
.foregroundColor(accentColor)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.foregroundColor(accentColor)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 12)
|
.padding(.top, 12)
|
||||||
|
|||||||
Reference in New Issue
Block a user