Liquid Glass theme with in-app appearance switcher (#1337)

* Centralize UI theme colors into semantic ThemePalette tokens

Introduce AppTheme/ThemePalette (Utils/Theme.swift) with an environment
key and @ThemedPalette property wrapper, persisted via @AppStorage.
Views now resolve background/primary/secondary/accentBlue/alertRed/
divider tokens from the environment instead of computing colors inline,
removing the backgroundColor/textColor/secondaryTextColor prop-drilling
through the header, composer, and people-sheet hierarchy.

Matrix theme output is pixel-identical; this is groundwork for a
user-selectable theme switcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add liquid glass theme with in-app appearance switcher

Add a .liquidGlass AppTheme alongside the matrix terminal theme,
selectable from a one-line APPEARANCE row in the app info sheet
(persisted via @AppStorage, applies live).

Liquid glass renders system fonts and colors over a subtle gradient
backdrop, with the header and composer floating as Liquid Glass panels
(real .glassEffect() on iOS/macOS 26, compiler-gated with an
ultraThinMaterial fallback for older SDKs). The message list scrolls
underneath the chrome via safe-area insets, and all sheets share the
same backdrop and surface language. The matrix theme is unchanged.

Details:
- Theme is threaded through ChatMessageFormatter so message
  AttributedStrings switch font design per theme; the per-message
  format cache gains a variant key so themes never serve each other's
  cached strings
- New palette tokens: accent (interactive tint) and locationAccent
  (geohash green), replacing scattered hardcoded greens/blues in the
  voice note, waveform, verification, and people-sheet views
- Header controls get full-height tap targets and the people count
  becomes a real Button (previously a tap gesture on the cluster)
- CommandSuggestionsView renders nothing when empty instead of a
  zero-height view that pushed the composer input off-center
- New appearance strings localized for all 29 catalog languages

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:
jack
2026-06-12 01:13:04 +02:00
committed by GitHub
co-authored by jack Claude Fable 5
parent c8381737bb
commit af954b05ea
27 changed files with 1347 additions and 498 deletions
+58 -40
View File
@@ -2,24 +2,24 @@ import SwiftUI
struct AppInfoView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
@ThemedPalette private var palette
@AppStorage(AppTheme.storageKey) private var appThemeRawValue = AppTheme.matrix.rawValue
private var selectedTheme: AppTheme {
AppTheme(rawValue: appThemeRawValue) ?? .matrix
}
private var backgroundColor: Color { palette.background }
private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary }
// MARK: - Constants
private enum Strings {
static let appName: LocalizedStringKey = "app_info.app_name"
static let tagline: LocalizedStringKey = "app_info.tagline"
static let appearanceTitle: LocalizedStringKey = "app_info.appearance.title"
enum Features {
static let title: LocalizedStringKey = "app_info.features.title"
@@ -101,12 +101,12 @@ struct AppInfoView: View {
.foregroundColor(textColor)
.padding()
}
.background(backgroundColor.opacity(0.95))
.themedSurface(opacity: 0.95)
ScrollView {
infoContent
}
.background(backgroundColor)
.themedSheetBackground()
}
.frame(width: 600, height: 700)
#else
@@ -114,13 +114,13 @@ struct AppInfoView: View {
ScrollView {
infoContent
}
.background(backgroundColor)
.themedSheetBackground()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.bitchatFont(size: 13, weight: .semibold)
.foregroundColor(textColor)
.frame(width: 32, height: 32)
}
@@ -138,16 +138,40 @@ struct AppInfoView: View {
// Header
VStack(alignment: .center, spacing: 8) {
Text(Strings.appName)
.font(.bitchatSystem(size: 32, weight: .bold, design: .monospaced))
.bitchatFont(size: 32, weight: .bold)
.foregroundColor(textColor)
Text(Strings.tagline)
.font(.bitchatSystem(size: 16, design: .monospaced))
.bitchatFont(size: 16)
.foregroundColor(secondaryTextColor)
}
.frame(maxWidth: .infinity)
.padding(.vertical)
// Appearance single row: label left, theme chips right
HStack(spacing: 12) {
SectionHeader(Strings.appearanceTitle)
Spacer()
ForEach(AppTheme.allCases) { theme in
Button {
appThemeRawValue = theme.rawValue
} label: {
Text(theme.displayNameKey)
.bitchatFont(size: 13, weight: selectedTheme == theme ? .semibold : .regular)
.foregroundColor(selectedTheme == theme ? palette.accent : secondaryTextColor)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(selectedTheme == theme ? palette.accent.opacity(0.15) : Color.clear)
)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityAddTraits(selectedTheme == theme ? .isSelected : [])
}
}
// How to Use
VStack(alignment: .leading, spacing: 16) {
SectionHeader(Strings.HowToUse.title)
@@ -157,7 +181,7 @@ struct AppInfoView: View {
Text(instruction)
}
}
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(textColor)
}
@@ -201,19 +225,17 @@ struct AppInfoFeatureInfo {
struct SectionHeader: View {
let title: LocalizedStringKey
@Environment(\.colorScheme) var colorScheme
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
@ThemedPalette private var palette
private var textColor: Color { palette.primary }
init(_ title: LocalizedStringKey) {
self.title = title
}
var body: some View {
Text(title)
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
.foregroundColor(textColor)
.padding(.top, 8)
}
@@ -221,16 +243,12 @@ struct SectionHeader: View {
struct FeatureRow: View {
let info: AppInfoFeatureInfo
@Environment(\.colorScheme) var colorScheme
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
}
@ThemedPalette private var palette
private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary }
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: info.icon)
@@ -240,11 +258,11 @@ struct FeatureRow: View {
VStack(alignment: .leading, spacing: 4) {
Text(info.title)
.font(.bitchatSystem(size: 14, weight: .semibold, design: .monospaced))
.bitchatFont(size: 14, weight: .semibold)
.foregroundColor(textColor)
Text(info.description)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(secondaryTextColor)
.fixedSize(horizontal: false, vertical: true)
}
@@ -10,13 +10,10 @@ import SwiftUI
struct CommandSuggestionsView: View {
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@ThemedPalette private var palette
@Binding var messageText: String
let textColor: Color
let backgroundColor: Color
let secondaryTextColor: Color
private var filteredCommands: [CommandInfo] {
guard messageText.hasPrefix("/") && !messageText.contains(" ") else { return [] }
let isGeoPublic = locationChannelsModel.selectedChannel.isLocation
@@ -27,42 +24,43 @@ struct CommandSuggestionsView: View {
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(filteredCommands) { command in
Button {
messageText = command.alias + " "
} label: {
buttonRow(for: command)
// Render nothing when there are no matches: a zero-height view would
// still receive the composer VStack's spacing and push the input row
// off-center.
if !filteredCommands.isEmpty {
VStack(alignment: .leading, spacing: 0) {
ForEach(filteredCommands) { command in
Button {
messageText = command.alias + " "
} label: {
buttonRow(for: command)
}
.buttonStyle(.plain)
.background(Color.gray.opacity(0.1))
}
.buttonStyle(.plain)
.background(Color.gray.opacity(0.1))
}
.themedOverlayPanel()
}
.background(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(secondaryTextColor.opacity(0.3), lineWidth: 1)
)
}
private func buttonRow(for command: CommandInfo) -> some View {
HStack {
Text(command.alias)
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 11)
.foregroundColor(palette.primary)
.fontWeight(.medium)
if let placeholder = command.placeholder {
Text(placeholder)
.font(.bitchatSystem(size: 10, design: .monospaced))
.foregroundColor(secondaryTextColor.opacity(0.8))
.bitchatFont(size: 10)
.foregroundColor(palette.secondary.opacity(0.8))
}
Spacer()
Text(command.description)
.font(.bitchatSystem(size: 10, design: .monospaced))
.foregroundColor(secondaryTextColor)
.bitchatFont(size: 10)
.foregroundColor(palette.secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 3)
@@ -85,12 +83,7 @@ struct CommandSuggestionsView: View {
)
let locationChannelsModel = LocationChannelsModel()
CommandSuggestionsView(
messageText: $messageText,
textColor: .green,
backgroundColor: .primary,
secondaryTextColor: .secondary
)
.environmentObject(privateConversationModel)
.environmentObject(locationChannelsModel)
CommandSuggestionsView(messageText: $messageText)
.environmentObject(privateConversationModel)
.environmentObject(locationChannelsModel)
}
@@ -10,18 +10,14 @@ import SwiftUI
import BitFoundation
struct DeliveryStatusView: View {
@Environment(\.colorScheme) private var colorScheme
@ThemedPalette private var palette
let status: DeliveryStatus
// MARK: - Computed Properties
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
}
private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary }
private enum Strings {
static func delivered(to nickname: String) -> String {
@@ -89,7 +85,7 @@ struct DeliveryStatusView: View {
Image(systemName: "checkmark")
.font(.bitchatSystem(size: 10, weight: .bold))
}
.foregroundColor(Color(red: 0.0, green: 0.478, blue: 1.0)) // Bright blue
.foregroundColor(palette.accentBlue)
.help(Strings.read(by: nickname))
case .failed(let reason):
@@ -103,7 +99,7 @@ struct DeliveryStatusView: View {
Image(systemName: "checkmark")
.font(.bitchatSystem(size: 10))
Text(verbatim: "\(reached)/\(total)")
.font(.bitchatSystem(size: 10, design: .monospaced))
.bitchatFont(size: 10)
}
.foregroundColor(secondaryTextColor.opacity(0.6))
.help(Strings.deliveredToMembers(reached, total))
@@ -11,6 +11,7 @@ import SwiftUI
struct PaymentChipView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.openURL) private var openURL
@ThemedPalette private var palette
enum PaymentType {
case cashu(String)
@@ -54,9 +55,7 @@ struct PaymentChipView: View {
let paymentType: PaymentType
private var fgColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var fgColor: Color { palette.primary }
private var bgColor: Color {
colorScheme == .dark ? Color.gray.opacity(0.18) : Color.gray.opacity(0.12)
}
@@ -73,7 +72,7 @@ struct PaymentChipView: View {
HStack(spacing: 6) {
Text(paymentType.emoji)
Text(paymentType.label)
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.bitchatFont(size: 12, weight: .semibold)
}
.padding(.vertical, 6)
.padding(.horizontal, 12)
@@ -11,6 +11,7 @@ import BitFoundation
struct TextMessageView: View {
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@Environment(\.appTheme) private var theme
@EnvironmentObject private var conversationUIModel: ConversationUIModel
let message: BitchatMessage
@@ -37,7 +38,7 @@ struct TextMessageView: View {
HStack(alignment: .top, spacing: 0) {
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty
let isExpanded = expandedMessageIDs.contains(message.id)
Text(conversationUIModel.formatMessage(message, colorScheme: colorScheme))
Text(conversationUIModel.formatMessage(message, colorScheme: colorScheme, theme: theme))
.fixedSize(horizontal: false, vertical: true)
.lineLimit(isLong && !isExpanded ? TransportConfig.uiLongMessageLineLimit : nil)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -58,7 +59,7 @@ struct TextMessageView: View {
if isExpanded { expandedMessageIDs.remove(message.id) }
else { expandedMessageIDs.insert(message.id) }
}
.font(.bitchatSystem(size: 11, weight: .medium, design: .monospaced))
.bitchatFont(size: 11, weight: .medium)
.foregroundColor(Color.blue)
.padding(.top, 4)
}
+15 -29
View File
@@ -6,16 +6,14 @@ import UIKit
struct ContentComposerView: View {
@EnvironmentObject private var conversationUIModel: ConversationUIModel
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.appTheme) private var theme
@ThemedPalette private var palette
@Binding var messageText: String
var isTextFieldFocused: FocusState<Bool>.Binding
@ObservedObject var voiceRecordingVM: VoiceRecordingViewModel
@Binding var autocompleteDebounceTimer: Timer?
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
let onSendMessage: () -> Void
#if os(iOS)
@@ -35,8 +33,8 @@ struct ContentComposerView: View {
}) {
HStack {
Text(suggestion)
.font(.bitchatSystem(size: 11, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 11)
.foregroundColor(palette.primary)
.fontWeight(.medium)
Spacer()
}
@@ -48,20 +46,11 @@ struct ContentComposerView: View {
.background(Color.gray.opacity(0.1))
}
}
.background(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(secondaryTextColor.opacity(0.3), lineWidth: 1)
)
.themedOverlayPanel()
.padding(.horizontal, 12)
}
CommandSuggestionsView(
messageText: $messageText,
textColor: textColor,
backgroundColor: backgroundColor,
secondaryTextColor: secondaryTextColor
)
CommandSuggestionsView(messageText: $messageText)
if voiceRecordingVM.state.isActive {
recordingIndicator
@@ -74,11 +63,11 @@ struct ContentComposerView: View {
prompt: Text(
String(localized: "content.input.message_placeholder", comment: "Placeholder shown in the chat composer")
)
.foregroundColor(secondaryTextColor.opacity(0.6))
.foregroundColor(palette.secondary.opacity(0.6))
)
.textFieldStyle(.plain)
.font(.bitchatSystem(size: 15, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 15)
.foregroundColor(palette.primary)
.focused(isTextFieldFocused)
.autocorrectionDisabled(true)
#if os(iOS)
@@ -86,12 +75,9 @@ struct ContentComposerView: View {
#endif
.submitLabel(.send)
.onSubmit(onSendMessage)
.padding(.vertical, 4)
.padding(.vertical, theme.usesGlassChrome ? 8 : 4)
.padding(.horizontal, 6)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(colorScheme == .dark ? Color.black.opacity(0.35) : Color.white.opacity(0.7))
)
.themedInputBackground()
.modifier(FocusEffectDisabledModifier())
.frame(maxWidth: .infinity, alignment: .leading)
.onChange(of: messageText) { newValue in
@@ -114,9 +100,9 @@ struct ContentComposerView: View {
}
}
.padding(.horizontal, 6)
.padding(.top, 6)
.padding(.top, theme.usesGlassChrome ? 8 : 6)
.padding(.bottom, 8)
.background(backgroundColor.opacity(0.95))
.themedChromePanel(edge: .bottom)
.onDisappear {
autocompleteDebounceTimer?.invalidate()
}
@@ -134,7 +120,7 @@ private extension ContentComposerView {
"recording \(voiceRecordingVM.formattedDuration(for: context.date))",
comment: "Voice note recording duration indicator"
)
.font(.bitchatSystem(size: 13, design: .monospaced))
.bitchatFont(size: 13)
.foregroundColor(.red)
}
Spacer()
@@ -154,7 +140,7 @@ private extension ContentComposerView {
}
var composerAccentColor: Color {
privateConversationModel.selectedPeerID != nil ? Color.orange : textColor
privateConversationModel.selectedPeerID != nil ? Color.orange : palette.accent
}
var attachmentButton: some View {
+67 -62
View File
@@ -8,8 +8,9 @@ struct ContentHeaderView: View {
@EnvironmentObject private var verificationModel: VerificationModel
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@EnvironmentObject private var peerListModel: PeerListModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@Environment(\.appTheme) private var theme
@ThemedPalette private var palette
@Binding var showSidebar: Bool
@Binding var showVerifySheet: Bool
@@ -20,15 +21,12 @@ struct ContentHeaderView: View {
let headerHeight: CGFloat
let headerPeerIconSize: CGFloat
let headerPeerCountFontSize: CGFloat
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
var body: some View {
HStack(spacing: 0) {
Text(verbatim: "bitchat/")
.font(.bitchatSystem(size: 18, weight: .medium, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 18, weight: .medium)
.foregroundColor(palette.primary)
.onTapGesture(count: 3) {
appChromeModel.panicClearAllData()
}
@@ -38,8 +36,8 @@ struct ContentHeaderView: View {
HStack(spacing: 0) {
Text(verbatim: "@")
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
TextField(
"content.input.nickname_placeholder",
@@ -49,9 +47,9 @@ struct ContentHeaderView: View {
)
)
.textFieldStyle(.plain)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.frame(maxWidth: 80)
.foregroundColor(textColor)
.foregroundColor(palette.primary)
.focused(isNicknameFieldFocused)
.autocorrectionDisabled(true)
#if os(iOS)
@@ -79,12 +77,13 @@ struct ContentHeaderView: View {
return countAndColor.0
}()
HStack(spacing: 10) {
HStack(spacing: 2) {
if appChromeModel.hasUnreadPrivateMessages {
Button(action: { appChromeModel.openMostRelevantPrivateChat() }) {
Image(systemName: "envelope.fill")
.font(.bitchatSystem(size: 12))
.foregroundColor(Color.orange)
.headerTapTarget()
}
.buttonStyle(.plain)
.accessibilityLabel(
@@ -99,13 +98,10 @@ struct ContentHeaderView: View {
notesGeohash = locationChannelsModel.currentBuildingGeohash
showLocationNotes = true
}) {
HStack(alignment: .center, spacing: 4) {
Image(systemName: "note.text")
.font(.bitchatSystem(size: 12))
.foregroundColor(Color.orange.opacity(0.8))
.padding(.top, 1)
}
.fixedSize(horizontal: true, vertical: false)
Image(systemName: "note.text")
.font(.bitchatSystem(size: 12))
.foregroundColor(Color.orange.opacity(0.8))
.headerTapTarget()
}
.buttonStyle(.plain)
.accessibilityLabel(
@@ -117,6 +113,7 @@ struct ContentHeaderView: View {
Button(action: { locationChannelsModel.toggleBookmark(channel.geohash) }) {
Image(systemName: locationChannelsModel.isBookmarked(channel.geohash) ? "bookmark.fill" : "bookmark")
.font(.bitchatSystem(size: 12))
.headerTapTarget()
}
.buttonStyle(.plain)
.accessibilityLabel(
@@ -140,49 +137,54 @@ struct ContentHeaderView: View {
case .mesh:
return Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
case .location:
return colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
return palette.locationAccent
}
}()
Text(badgeText)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(badgeColor)
.lineLimit(headerLineLimit)
.fixedSize(horizontal: true, vertical: false)
.layoutPriority(2)
.padding(.horizontal, 6)
.frame(maxHeight: .infinity)
.contentShape(Rectangle())
.accessibilityLabel(
String(localized: "content.accessibility.location_channels", comment: "Accessibility label for the location channels button")
)
}
.buttonStyle(.plain)
.padding(.leading, 4)
.padding(.trailing, 2)
HStack(spacing: 4) {
Image(systemName: "person.2.fill")
.font(.system(size: headerPeerIconSize, weight: .regular))
.accessibilityLabel(
String(
format: String(localized: "content.accessibility.people_count", comment: "Accessibility label announcing number of people in header"),
locale: .current,
headerOtherPeersCount
)
)
Text("\(headerOtherPeersCount)")
.font(.system(size: headerPeerCountFontSize, weight: .regular, design: .monospaced))
.accessibilityHidden(true)
Button(action: {
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar.toggle()
}
}) {
HStack(spacing: 4) {
Image(systemName: "person.2.fill")
.font(.system(size: headerPeerIconSize, weight: .regular))
Text("\(headerOtherPeersCount)")
.font(.system(size: headerPeerCountFontSize, weight: .regular, design: theme.bodyFontDesign))
.accessibilityHidden(true)
}
.foregroundColor(headerCountColor)
.lineLimit(headerLineLimit)
.fixedSize(horizontal: true, vertical: false)
.padding(.leading, 6)
.frame(maxHeight: .infinity)
.contentShape(Rectangle())
}
.foregroundColor(headerCountColor)
.padding(.leading, 2)
.lineLimit(headerLineLimit)
.fixedSize(horizontal: true, vertical: false)
.buttonStyle(.plain)
.accessibilityLabel(
String(
format: String(localized: "content.accessibility.people_count", comment: "Accessibility label announcing number of people in header"),
locale: .current,
headerOtherPeersCount
)
)
}
.layoutPriority(3)
.onTapGesture {
withAnimation(.easeInOut(duration: TransportConfig.uiAnimationMediumSeconds)) {
showSidebar.toggle()
}
}
.sheet(isPresented: $showVerifySheet) {
VerificationSheetView(isPresented: $showVerifySheet)
.environmentObject(verificationModel)
@@ -208,10 +210,7 @@ struct ContentHeaderView: View {
} else {
ContentLocationNotesUnavailableView(
showLocationNotes: $showLocationNotes,
headerHeight: headerHeight,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor
headerHeight: headerHeight
)
.environmentObject(locationChannelsModel)
}
@@ -249,7 +248,16 @@ struct ContentHeaderView: View {
} message: {
Text("content.alert.screenshot.message")
}
.background(backgroundColor.opacity(0.95))
.themedChromePanel(edge: .top)
}
}
private extension View {
/// Expands a small header icon to a comfortably tappable, full-bar-height
/// hit area without changing its visual size.
func headerTapTarget() -> some View {
frame(minWidth: 30, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
@@ -262,8 +270,7 @@ private extension ContentHeaderView {
switch locationChannelsModel.selectedChannel {
case .location:
let count = peerListModel.visibleGeohashPeerCount
let standardGreen = colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
return (count, count > 0 ? standardGreen : Color.secondary)
return (count, count > 0 ? palette.locationAccent : Color.secondary)
case .mesh:
let meshBlue = Color(hue: 0.60, saturation: 0.85, brightness: 0.82)
let color: Color = peerListModel.connectedMeshPeerCount > 0 ? meshBlue : Color.secondary
@@ -274,24 +281,22 @@ private extension ContentHeaderView {
private struct ContentLocationNotesUnavailableView: View {
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@ThemedPalette private var palette
@Binding var showLocationNotes: Bool
let headerHeight: CGFloat
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
var body: some View {
VStack(spacing: 12) {
HStack {
Text("content.notes.title")
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
Spacer()
Button(action: { showLocationNotes = false }) {
Image(systemName: "xmark")
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 13, weight: .semibold)
.foregroundColor(palette.primary)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -299,17 +304,17 @@ private struct ContentLocationNotesUnavailableView: View {
}
.frame(height: headerHeight)
.padding(.horizontal, 12)
.background(backgroundColor.opacity(0.95))
.themedChromePanel(edge: .top)
Text("content.notes.location_unavailable")
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
Button("content.location.enable") {
locationChannelsModel.enableAndRefresh()
}
.buttonStyle(.bordered)
Spacer()
}
.background(backgroundColor)
.foregroundColor(textColor)
.themedSheetBackground()
.foregroundColor(palette.primary)
}
}
+31 -54
View File
@@ -25,10 +25,8 @@ struct ContentPeopleSheetView: View {
var isTextFieldFocused: FocusState<Bool>.Binding
@ObservedObject var voiceRecordingVM: VoiceRecordingViewModel
@Binding var autocompleteDebounceTimer: Timer?
@ThemedPalette private var palette
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
let headerHeight: CGFloat
let onSendMessage: () -> Void
@@ -56,9 +54,6 @@ struct ContentPeopleSheetView: View {
isTextFieldFocused: isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
headerHeight: headerHeight,
onSendMessage: onSendMessage,
showImagePicker: $showImagePicker,
@@ -77,9 +72,6 @@ struct ContentPeopleSheetView: View {
isTextFieldFocused: isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
headerHeight: headerHeight,
onSendMessage: onSendMessage,
showMacImagePicker: $showMacImagePicker
@@ -88,9 +80,6 @@ struct ContentPeopleSheetView: View {
} else {
ContentPeopleListView(
showSidebar: $showSidebar,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
headerHeight: headerHeight
)
}
@@ -109,8 +98,8 @@ struct ContentPeopleSheetView: View {
}
}
}
.background(backgroundColor)
.foregroundColor(textColor)
.themedSheetBackground()
.foregroundColor(palette.primary)
#if os(macOS)
.frame(minWidth: 420, minHeight: 520)
#endif
@@ -148,12 +137,10 @@ private struct ContentPeopleListView: View {
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@EnvironmentObject private var peerListModel: PeerListModel
@Environment(\.dismiss) private var dismiss
@ThemedPalette private var palette
@Binding var showSidebar: Bool
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
let headerHeight: CGFloat
@State private var showVerifySheet = false
@@ -163,8 +150,8 @@ private struct ContentPeopleListView: View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 12) {
Text(peopleSheetTitle)
.font(.bitchatSystem(size: 18, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 18)
.foregroundColor(palette.primary)
Spacer()
if case .mesh = locationChannelsModel.selectedChannel {
Button(action: { showVerifySheet = true }) {
@@ -185,7 +172,7 @@ private struct ContentPeopleListView: View {
}
}) {
Image(systemName: "xmark")
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.bitchatFont(size: 12, weight: .semibold)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -201,9 +188,9 @@ private struct ContentPeopleListView: View {
let subtitleColor: Color = {
switch locationChannelsModel.selectedChannel {
case .mesh:
return Color.blue
return palette.accentBlue
case .location:
return Color.green
return palette.locationAccent
}
}()
@@ -213,32 +200,28 @@ private struct ContentPeopleListView: View {
Text(activeText)
.foregroundColor(.secondary)
}
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
} else {
Text(activeText)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
}
}
.padding(.horizontal, 16)
.padding(.top, 16)
.padding(.bottom, 12)
.background(backgroundColor)
.themedSurface()
ScrollView {
VStack(alignment: .leading, spacing: 6) {
if case .location = locationChannelsModel.selectedChannel {
GeohashPeopleList(
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onTapPerson: {
showSidebar = true
}
)
} else {
MeshPeerList(
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onTapPeer: { peerID in
peerListModel.startConversation(with: peerID)
showSidebar = true
@@ -302,10 +285,9 @@ private struct ContentPrivateChatSheetView: View {
var isTextFieldFocused: FocusState<Bool>.Binding
@ObservedObject var voiceRecordingVM: VoiceRecordingViewModel
@Binding var autocompleteDebounceTimer: Timer?
@Environment(\.appTheme) private var theme
@ThemedPalette private var palette
let backgroundColor: Color
let textColor: Color
let secondaryTextColor: Color
let headerHeight: CGFloat
let onSendMessage: () -> Void
@@ -327,7 +309,7 @@ private struct ContentPrivateChatSheetView: View {
}) {
Image(systemName: "chevron.left")
.font(.bitchatSystem(size: 12))
.foregroundColor(textColor)
.foregroundColor(palette.primary)
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
@@ -341,8 +323,7 @@ private struct ContentPrivateChatSheetView: View {
HStack(spacing: 8) {
ContentPrivateHeaderInfoButton(
headerState: headerState,
headerHeight: headerHeight,
textColor: textColor
headerHeight: headerHeight
)
if headerState.supportsFavoriteToggle {
@@ -351,7 +332,7 @@ private struct ContentPrivateChatSheetView: View {
}) {
Image(systemName: headerState.isFavorite ? "star.fill" : "star")
.font(.bitchatSystem(size: 14))
.foregroundColor(headerState.isFavorite ? Color.yellow : textColor)
.foregroundColor(headerState.isFavorite ? Color.yellow : palette.primary)
}
.buttonStyle(.plain)
.accessibilityLabel(
@@ -372,7 +353,7 @@ private struct ContentPrivateChatSheetView: View {
}
}) {
Image(systemName: "xmark")
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.bitchatFont(size: 12, weight: .semibold)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -382,7 +363,7 @@ private struct ContentPrivateChatSheetView: View {
.padding(.horizontal, 16)
.padding(.top, 10)
.padding(.bottom, 12)
.background(backgroundColor)
.themedSurface()
}
MessageListView(
@@ -397,10 +378,12 @@ private struct ContentPrivateChatSheetView: View {
showSidebar: $showSidebar,
isTextFieldFocused: isTextFieldFocused
)
.background(backgroundColor)
.themedSurface()
.frame(maxWidth: .infinity, maxHeight: .infinity)
Divider()
if !theme.usesGlassChrome {
Divider()
}
#if os(iOS)
ContentComposerView(
@@ -408,9 +391,6 @@ private struct ContentPrivateChatSheetView: View {
isTextFieldFocused: isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onSendMessage: onSendMessage,
showImagePicker: $showImagePicker,
imagePickerSourceType: $imagePickerSourceType
@@ -421,16 +401,13 @@ private struct ContentPrivateChatSheetView: View {
isTextFieldFocused: isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onSendMessage: onSendMessage,
showMacImagePicker: $showMacImagePicker
)
#endif
}
.background(backgroundColor)
.foregroundColor(textColor)
.themedSheetBackground()
.foregroundColor(palette.primary)
.highPriorityGesture(
DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onEnded { value in
@@ -448,10 +425,10 @@ private struct ContentPrivateChatSheetView: View {
private struct ContentPrivateHeaderInfoButton: View {
@EnvironmentObject private var appChromeModel: AppChromeModel
@ThemedPalette private var palette
let headerState: PrivateConversationHeaderState
let headerHeight: CGFloat
let textColor: Color
var body: some View {
Button(action: {
@@ -462,12 +439,12 @@ private struct ContentPrivateHeaderInfoButton: View {
case .bluetoothConnected:
Image(systemName: "dot.radiowaves.left.and.right")
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.foregroundColor(palette.primary)
.accessibilityLabel(String(localized: "content.accessibility.connected_mesh", comment: "Accessibility label for mesh-connected peer indicator"))
case .meshReachable:
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
.font(.bitchatSystem(size: 14))
.foregroundColor(textColor)
.foregroundColor(palette.primary)
.accessibilityLabel(String(localized: "content.accessibility.reachable_mesh", comment: "Accessibility label for mesh-reachable peer indicator"))
case .nostrAvailable:
Image(systemName: "globe")
@@ -479,8 +456,8 @@ private struct ContentPrivateHeaderInfoButton: View {
}
Text(headerState.displayName)
.font(.bitchatSystem(size: 16, weight: .medium, design: .monospaced))
.foregroundColor(textColor)
.bitchatFont(size: 16, weight: .medium)
.foregroundColor(palette.primary)
if let encryptionStatus = headerState.encryptionStatus,
let icon = encryptionStatus.icon {
@@ -488,7 +465,7 @@ private struct ContentPrivateHeaderInfoButton: View {
.font(.bitchatSystem(size: 14))
.foregroundColor(
encryptionStatus == .noiseVerified || encryptionStatus == .noiseSecured
? textColor
? palette.primary
: Color.red
)
.accessibilityLabel(
+100 -86
View File
@@ -40,6 +40,7 @@ struct ContentView: View {
@State private var messageText = ""
@FocusState private var isTextFieldFocused: Bool
@Environment(\.colorScheme) var colorScheme
@Environment(\.appTheme) private var appTheme
@State private var showSidebar = false
@State private var selectedMessageSender: String?
@State private var selectedMessageSenderID: PeerID?
@@ -63,39 +64,19 @@ struct ContentView: View {
@State private var windowCountPublic: Int = 300
@State private var windowCountPrivate: [PeerID: Int] = [:]
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
}
@ThemedPalette private var palette
private var selectedPrivatePeerID: PeerID? {
privateConversationModel.selectedPeerID
}
private var usesGlassLayout: Bool { appTheme.usesGlassChrome }
var body: some View {
VStack(spacing: 0) {
ContentHeaderView(
showSidebar: $showSidebar,
showVerifySheet: $showVerifySheet,
showLocationNotes: $showLocationNotes,
notesGeohash: $notesGeohash,
isNicknameFieldFocused: $isNicknameFieldFocused,
headerHeight: headerHeight,
headerPeerIconSize: headerPeerIconSize,
headerPeerCountFontSize: headerPeerCountFontSize,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor
)
mainContent
.onAppear {
conversationUIModel.setCurrentColorScheme(colorScheme)
conversationUIModel.setCurrentTheme(appTheme)
#if os(macOS)
DispatchQueue.main.async {
isNicknameFieldFocused = false
@@ -106,62 +87,11 @@ struct ContentView: View {
.onChange(of: colorScheme) { newValue in
conversationUIModel.setCurrentColorScheme(newValue)
}
Divider()
GeometryReader { geometry in
VStack(spacing: 0) {
MessageListView(
privatePeer: nil,
isAtBottom: $isAtBottomPublic,
messageText: $messageText,
selectedMessageSender: $selectedMessageSender,
selectedMessageSenderID: $selectedMessageSenderID,
imagePreviewURL: $imagePreviewURL,
windowCountPublic: $windowCountPublic,
windowCountPrivate: $windowCountPrivate,
showSidebar: $showSidebar,
isTextFieldFocused: $isTextFieldFocused
)
.background(backgroundColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(width: geometry.size.width, height: geometry.size.height)
.onChange(of: appTheme) { newValue in
conversationUIModel.setCurrentTheme(newValue)
}
Divider()
if selectedPrivatePeerID == nil {
#if os(iOS)
ContentComposerView(
messageText: $messageText,
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onSendMessage: sendMessage,
showImagePicker: $showImagePicker,
imagePickerSourceType: $imagePickerSourceType
)
#else
ContentComposerView(
messageText: $messageText,
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
onSendMessage: sendMessage,
showMacImagePicker: $showMacImagePicker
)
#endif
}
}
.background(backgroundColor)
.foregroundColor(textColor)
.background(ThemedRootBackground())
.foregroundColor(palette.primary)
#if os(macOS)
.frame(minWidth: 600, minHeight: 400)
#endif
@@ -194,9 +124,6 @@ struct ContentView: View {
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
headerHeight: headerHeight,
onSendMessage: sendMessage,
showImagePicker: $showImagePicker,
@@ -215,9 +142,6 @@ struct ContentView: View {
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: backgroundColor,
textColor: textColor,
secondaryTextColor: secondaryTextColor,
headerHeight: headerHeight,
onSendMessage: sendMessage,
showMacImagePicker: $showMacImagePicker
@@ -302,6 +226,96 @@ struct ContentView: View {
}
}
/// Matrix: classic opaque bars with dividers. Glass: full-bleed message
/// list scrolling underneath floating chrome panels (safe-area insets),
/// so the translucency gains usable space instead of losing it.
@ViewBuilder
private var mainContent: some View {
if usesGlassLayout {
publicMessageList
.safeAreaInset(edge: .top, spacing: 0) {
headerView
}
.safeAreaInset(edge: .bottom, spacing: 0) {
if selectedPrivatePeerID == nil {
composerView
}
}
} else {
VStack(spacing: 0) {
headerView
Divider()
GeometryReader { geometry in
VStack(spacing: 0) {
publicMessageList
.background(palette.background)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
Divider()
if selectedPrivatePeerID == nil {
composerView
}
}
}
}
private var headerView: some View {
ContentHeaderView(
showSidebar: $showSidebar,
showVerifySheet: $showVerifySheet,
showLocationNotes: $showLocationNotes,
notesGeohash: $notesGeohash,
isNicknameFieldFocused: $isNicknameFieldFocused,
headerHeight: headerHeight,
headerPeerIconSize: headerPeerIconSize,
headerPeerCountFontSize: headerPeerCountFontSize
)
}
private var publicMessageList: some View {
MessageListView(
privatePeer: nil,
isAtBottom: $isAtBottomPublic,
messageText: $messageText,
selectedMessageSender: $selectedMessageSender,
selectedMessageSenderID: $selectedMessageSenderID,
imagePreviewURL: $imagePreviewURL,
windowCountPublic: $windowCountPublic,
windowCountPrivate: $windowCountPrivate,
showSidebar: $showSidebar,
isTextFieldFocused: $isTextFieldFocused
)
}
private var composerView: some View {
#if os(iOS)
ContentComposerView(
messageText: $messageText,
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
onSendMessage: sendMessage,
showImagePicker: $showImagePicker,
imagePickerSourceType: $imagePickerSourceType
)
#else
ContentComposerView(
messageText: $messageText,
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
onSendMessage: sendMessage,
showMacImagePicker: $showMacImagePicker
)
#endif
}
private func sendMessage() {
guard let trimmed = messageText.trimmedOrNilIfEmpty else { return }
+18 -22
View File
@@ -13,15 +13,11 @@ struct FingerprintView: View {
@EnvironmentObject private var verificationModel: VerificationModel
let peerID: PeerID
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
@ThemedPalette private var palette
private var textColor: Color { palette.primary }
private var backgroundColor: Color { palette.background }
private enum Strings {
static let title: LocalizedStringKey = "fingerprint.title"
@@ -53,7 +49,7 @@ struct FingerprintView: View {
// Header
HStack {
Text(Strings.title)
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
.foregroundColor(textColor)
Spacer()
@@ -76,11 +72,11 @@ struct FingerprintView: View {
VStack(alignment: .leading, spacing: 4) {
Text(fingerprintState.peerNickname)
.font(.bitchatSystem(size: 18, weight: .semibold, design: .monospaced))
.bitchatFont(size: 18, weight: .semibold)
.foregroundColor(textColor)
Text(fingerprintState.encryptionStatus.description)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(textColor.opacity(0.7))
}
@@ -93,12 +89,12 @@ struct FingerprintView: View {
// Their fingerprint
VStack(alignment: .leading, spacing: 8) {
Text(Strings.theirFingerprint)
.font(.bitchatSystem(size: 12, weight: .bold, design: .monospaced))
.bitchatFont(size: 12, weight: .bold)
.foregroundColor(textColor.opacity(0.7))
if let fingerprint = fingerprintState.theirFingerprint {
Text(formatFingerprint(fingerprint))
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(textColor)
.multilineTextAlignment(.leading)
.lineLimit(nil)
@@ -119,7 +115,7 @@ struct FingerprintView: View {
}
} else {
Text(Strings.handshakePending)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(Color.orange)
.padding()
}
@@ -128,11 +124,11 @@ struct FingerprintView: View {
// My fingerprint
VStack(alignment: .leading, spacing: 8) {
Text(Strings.yourFingerprint)
.font(.bitchatSystem(size: 12, weight: .bold, design: .monospaced))
.bitchatFont(size: 12, weight: .bold)
.foregroundColor(textColor.opacity(0.7))
Text(formatFingerprint(fingerprintState.myFingerprint))
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(textColor)
.multilineTextAlignment(.leading)
.lineLimit(nil)
@@ -157,7 +153,7 @@ struct FingerprintView: View {
if fingerprintState.canToggleVerification {
VStack(spacing: 12) {
Text(fingerprintState.isVerified ? Strings.verifiedBadge : Strings.notVerifiedBadge)
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.bitchatFont(size: 14, weight: .bold)
.foregroundColor(fingerprintState.isVerified ? Color.green : Color.orange)
.frame(maxWidth: .infinity)
@@ -168,7 +164,7 @@ struct FingerprintView: View {
Text(Strings.verifyHint(fingerprintState.peerNickname))
}
}
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(textColor.opacity(0.7))
.multilineTextAlignment(.center)
.lineLimit(nil)
@@ -181,7 +177,7 @@ struct FingerprintView: View {
dismiss()
}) {
Text(Strings.markVerified)
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.bitchatFont(size: 14, weight: .bold)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
@@ -195,7 +191,7 @@ struct FingerprintView: View {
dismiss()
}) {
Text(Strings.removeVerification)
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.bitchatFont(size: 14, weight: .bold)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
@@ -216,7 +212,7 @@ struct FingerprintView: View {
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(backgroundColor)
.themedSheetBackground()
}
private func formatFingerprint(_ fingerprint: String) -> String {
+6 -7
View File
@@ -2,8 +2,7 @@ import SwiftUI
struct GeohashPeopleList: View {
@EnvironmentObject private var peerListModel: PeerListModel
let textColor: Color
let secondaryTextColor: Color
@ThemedPalette private var palette
let onTapPerson: () -> Void
@Environment(\.colorScheme) var colorScheme
@State private var orderedIDs: [String] = []
@@ -20,8 +19,8 @@ struct GeohashPeopleList: View {
if peerListModel.geohashPeople.isEmpty {
VStack(alignment: .leading, spacing: 0) {
Text(Strings.noneNearby)
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
.padding(.horizontal)
.padding(.top, 12)
}
@@ -52,18 +51,18 @@ struct GeohashPeopleList: View {
let (base, suffix) = person.displayName.splitSuffix()
HStack(spacing: 0) {
Text(base)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.fontWeight(person.isMe ? .bold : .regular)
.foregroundColor(rowColor)
if !suffix.isEmpty {
let suffixColor = person.isMe ? Color.orange.opacity(0.6) : rowColor.opacity(0.6)
Text(suffix)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(suffixColor)
}
if person.isMe {
Text(Strings.youSuffix)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(rowColor)
}
}
+31 -37
View File
@@ -9,11 +9,11 @@ struct LocationChannelsSheet: View {
@Binding var isPresented: Bool
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@EnvironmentObject private var peerListModel: PeerListModel
@Environment(\.colorScheme) var colorScheme
@ThemedPalette private var palette
@State private var customGeohash: String = ""
@State private var customError: String? = nil
private var backgroundColor: Color { colorScheme == .dark ? .black : .white }
private var backgroundColor: Color { palette.background }
private enum Strings {
static let title: LocalizedStringKey = "location_channels.title"
@@ -97,12 +97,12 @@ struct LocationChannelsSheet: View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
Text(Strings.title)
.font(.bitchatSystem(size: 18, design: .monospaced))
.bitchatFont(size: 18)
Spacer()
closeButton
}
Text(Strings.description)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
Group {
@@ -110,7 +110,7 @@ struct LocationChannelsSheet: View {
case .notDetermined:
Button(action: { locationChannelsModel.enableLocationChannels() }) {
Text(Strings.requestPermissions)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(standardGreen)
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
@@ -121,7 +121,7 @@ struct LocationChannelsSheet: View {
case .denied, .restricted:
VStack(alignment: .leading, spacing: 8) {
Text(Strings.permissionDenied)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
Button(Strings.openSettings, action: SystemSettings.location.open)
.buttonStyle(.plain)
@@ -136,7 +136,7 @@ struct LocationChannelsSheet: View {
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(backgroundColor)
.themedSurface()
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
@@ -147,7 +147,7 @@ struct LocationChannelsSheet: View {
#if os(macOS)
.frame(minWidth: 420, minHeight: 520)
#endif
.background(backgroundColor)
.themedSheetBackground()
.onAppear {
// Refresh channels when opening
if locationChannelsModel.permissionState == .authorized {
@@ -171,7 +171,7 @@ struct LocationChannelsSheet: View {
private var closeButton: some View {
Button(action: { isPresented = false }) {
Image(systemName: "xmark")
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.bitchatFont(size: 13, weight: .semibold)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -223,7 +223,7 @@ struct LocationChannelsSheet: View {
HStack(spacing: 8) {
ProgressView()
Text(Strings.loadingNearby)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 10)
@@ -246,8 +246,8 @@ struct LocationChannelsSheet: View {
.padding(.top, 12)
Button(action: SystemSettings.location.open) {
Text(Strings.removeAccess)
.font(.bitchatSystem(size: 12, design: .monospaced))
.foregroundColor(Color(red: 0.75, green: 0.1, blue: 0.1))
.bitchatFont(size: 12)
.foregroundColor(palette.alertRed)
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
.background(Color.red.opacity(0.08))
@@ -259,9 +259,9 @@ struct LocationChannelsSheet: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 6)
.background(backgroundColor)
.themedSurface()
}
.background(backgroundColor)
.themedSurface()
}
private var sectionDivider: some View {
@@ -270,15 +270,13 @@ struct LocationChannelsSheet: View {
.frame(height: 1)
}
private var dividerColor: Color {
colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08)
}
private var dividerColor: Color { palette.divider }
private var customTeleportSection: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 2) {
Text(verbatim: "#")
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(.secondary)
TextField("geohash", text: $customGeohash)
#if os(iOS)
@@ -286,7 +284,7 @@ struct LocationChannelsSheet: View {
.autocorrectionDisabled(true)
.keyboardType(.asciiCapable)
#endif
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.onChange(of: customGeohash) { newValue in
let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz")
let filtered = newValue
@@ -312,13 +310,13 @@ struct LocationChannelsSheet: View {
}) {
HStack(spacing: 6) {
Text(Strings.teleport)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
Image(systemName: "face.dashed")
.font(.bitchatSystem(size: 14))
}
}
.buttonStyle(.plain)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color.secondary.opacity(0.12))
@@ -328,7 +326,7 @@ struct LocationChannelsSheet: View {
}
if let err = customError {
Text(err)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.red)
}
}
@@ -337,7 +335,7 @@ struct LocationChannelsSheet: View {
private func bookmarkedSection(_ entries: [String]) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(Strings.bookmarked)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
LazyVStack(spacing: 0) {
ForEach(Array(entries.enumerated()), id: \.offset) { index, gh in
@@ -409,18 +407,18 @@ struct LocationChannelsSheet: View {
let parts = splitTitleAndCount(title)
HStack(spacing: 4) {
Text(parts.base)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.fontWeight(titleBold ? .bold : .regular)
.foregroundColor(titleColor ?? Color.primary)
if let count = parts.countSuffix, !count.isEmpty {
Text(count)
.font(.bitchatSystem(size: 11, design: .monospaced))
.bitchatFont(size: 11)
.foregroundColor(.secondary)
}
}
let subtitleFull = Strings.subtitle(prefix: subtitlePrefix, name: subtitleName)
Text(subtitleFull)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
@@ -428,7 +426,7 @@ struct LocationChannelsSheet: View {
Spacer()
if isSelected {
Text(verbatim: "✔︎")
.font(.bitchatSystem(size: 16, design: .monospaced))
.bitchatFont(size: 16)
.foregroundColor(standardGreen)
}
trailingAccessory()
@@ -478,26 +476,22 @@ extension LocationChannelsSheet {
Toggle(isOn: torToggleBinding) {
VStack(alignment: .leading, spacing: 2) {
Text(Strings.torTitle)
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.bitchatFont(size: 12, weight: .semibold)
.foregroundColor(.primary)
Text(Strings.torSubtitle)
.font(.bitchatSystem(size: 11, design: .monospaced))
.bitchatFont(size: 11)
.foregroundColor(.secondary)
}
}
.toggleStyle(IRCToggleStyle(accent: standardGreen, onLabel: Strings.toggleOn, offLabel: Strings.toggleOff))
.toggleStyle(IRCToggleStyle(accent: palette.accent, onLabel: Strings.toggleOn, offLabel: Strings.toggleOff))
}
.padding(12)
.background(Color.secondary.opacity(0.12))
.cornerRadius(8)
}
private var standardGreen: Color {
(colorScheme == .dark) ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var standardBlue: Color {
Color(red: 0.0, green: 0.478, blue: 1.0)
}
private var standardGreen: Color { palette.primary }
private var standardBlue: Color { palette.accentBlue }
}
private struct IRCToggleStyle: ToggleStyle {
@@ -512,7 +506,7 @@ private struct IRCToggleStyle: ToggleStyle {
Spacer()
Text(configuration.isOn ? onLabel : offLabel)
.textCase(.uppercase)
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.bitchatFont(size: 12, weight: .semibold)
.foregroundColor(configuration.isOn ? accent : .secondary)
.padding(.vertical, 4)
.padding(.horizontal, 10)
+28 -28
View File
@@ -6,7 +6,7 @@ struct LocationNotesView: View {
let senderNickname: String
let onNotesCountChanged: ((Int) -> Void)?
@Environment(\.colorScheme) var colorScheme
@ThemedPalette private var palette
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@Environment(\.dismiss) private var dismiss
@@ -25,8 +25,8 @@ struct LocationNotesView: View {
_manager = StateObject(wrappedValue: manager ?? LocationNotesManager(geohash: gh))
}
private var backgroundColor: Color { colorScheme == .dark ? .black : .white }
private var accentGreen: Color { colorScheme == .dark ? .green : Color(red: 0, green: 0.5, blue: 0) }
private var backgroundColor: Color { palette.background }
private var accentGreen: Color { palette.accent }
private var maxDraftLines: Int { dynamicTypeSize.isAccessibilitySize ? 5 : 3 }
private enum Strings {
@@ -53,11 +53,11 @@ struct LocationNotesView: View {
notesContent
}
}
.background(backgroundColor)
.themedSurface()
inputSection
}
.frame(minWidth: 420, idealWidth: 440, minHeight: 620, idealHeight: 680)
.background(backgroundColor)
.themedSheetBackground()
.onDisappear { manager.cancel() }
.onChange(of: geohash) { newValue in
manager.setGeohash(newValue)
@@ -76,7 +76,7 @@ struct LocationNotesView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
inputSection
}
.background(backgroundColor)
.themedSurface()
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
@@ -84,7 +84,7 @@ struct LocationNotesView: View {
.navigationTitle("")
#endif
}
.background(backgroundColor)
.themedSheetBackground()
.onDisappear { manager.cancel() }
.onChange(of: geohash) { newValue in
manager.setGeohash(newValue)
@@ -99,7 +99,7 @@ struct LocationNotesView: View {
private var closeButton: some View {
Button(action: { dismiss() }) {
Image(systemName: "xmark")
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.bitchatFont(size: 13, weight: .semibold)
.frame(width: 32, height: 32)
}
.buttonStyle(.plain)
@@ -111,33 +111,33 @@ struct LocationNotesView: View {
return VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 12) {
Text(headerTitle(for: count))
.font(.bitchatSystem(size: 18, design: .monospaced))
.bitchatFont(size: 18)
Spacer()
closeButton
}
if let building = locationChannelsModel.locationName(for: .building), !building.isEmpty {
Text(building)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(accentGreen)
} else if let block = locationChannelsModel.locationName(for: .block), !block.isEmpty {
Text(block)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(accentGreen)
}
Text(Strings.description)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
if manager.state == .noRelays {
Text(Strings.relaysPaused)
.font(.bitchatSystem(size: 11, design: .monospaced))
.bitchatFont(size: 11)
.foregroundColor(.secondary)
}
}
.padding(.horizontal, 16)
.padding(.top, 16)
.padding(.bottom, 12)
.background(backgroundColor)
.themedSurface()
}
private func headerTitle(for count: Int) -> String {
@@ -176,16 +176,16 @@ struct LocationNotesView: View {
return VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(verbatim: "@\(baseName)")
.font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced))
.bitchatFont(size: 12, weight: .semibold)
if !ts.isEmpty {
Text(ts)
.font(.bitchatSystem(size: 11, design: .monospaced))
.bitchatFont(size: 11)
.foregroundColor(.secondary)
}
Spacer()
}
Text(note.content)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 4)
@@ -194,12 +194,12 @@ struct LocationNotesView: View {
private var noRelaysRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text(Strings.noRelaysNearby)
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.bitchatFont(size: 13, weight: .semibold)
Text(Strings.relaysRetryHint)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
Button(Strings.retry) { manager.refresh() }
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.buttonStyle(.plain)
}
.padding(.vertical, 6)
@@ -209,7 +209,7 @@ struct LocationNotesView: View {
HStack(spacing: 10) {
ProgressView()
Text(Strings.loadingNotes)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
Spacer()
}
@@ -219,9 +219,9 @@ struct LocationNotesView: View {
private var emptyRow: some View {
VStack(alignment: .leading, spacing: 4) {
Text(Strings.emptyTitle)
.font(.bitchatSystem(size: 13, weight: .semibold, design: .monospaced))
.bitchatFont(size: 13, weight: .semibold)
Text(Strings.emptySubtitle)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.secondary)
}
.padding(.vertical, 6)
@@ -231,13 +231,13 @@ struct LocationNotesView: View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
Text(message)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
Spacer()
}
Button(Strings.dismissError) { manager.clearError() }
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.buttonStyle(.plain)
}
.padding(.vertical, 6)
@@ -247,7 +247,7 @@ struct LocationNotesView: View {
HStack(alignment: .top, spacing: 10) {
TextField(Strings.addPlaceholder, text: $draft, axis: .vertical)
.textFieldStyle(.plain)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.lineLimit(maxDraftLines, reservesSpace: true)
.padding(.vertical, 6)
Button(action: send) {
@@ -261,7 +261,7 @@ struct LocationNotesView: View {
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(backgroundColor)
.themedSurface()
.overlay(Divider(), alignment: .top)
}
+2 -1
View File
@@ -10,6 +10,7 @@ import BitFoundation
struct MediaMessageView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.appTheme) private var theme
@EnvironmentObject private var conversationUIModel: ConversationUIModel
let message: BitchatMessage
let media: BitchatMessage.Media
@@ -36,7 +37,7 @@ struct MediaMessageView: View {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .center, spacing: 4) {
Text(conversationUIModel.formatMessageHeader(message, colorScheme: colorScheme))
Text(conversationUIModel.formatMessageHeader(message, colorScheme: colorScheme, theme: theme))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
if message.isPrivate && conversationUIModel.isSentByCurrentUser(message),
+4 -3
View File
@@ -8,6 +8,7 @@ struct VoiceNoteView: View {
private let onCancel: (() -> Void)?
@Environment(\.colorScheme) private var colorScheme
@ThemedPalette private var palette
@StateObject private var playback: VoiceNotePlaybackController
@State private var waveform: [Float] = []
@@ -31,7 +32,7 @@ struct VoiceNoteView: View {
}
private var borderColor: Color {
colorScheme == .dark ? Color.green.opacity(0.3) : Color.green.opacity(0.2)
colorScheme == .dark ? palette.accent.opacity(0.3) : palette.accent.opacity(0.2)
}
private var playbackLabel: String {
@@ -46,7 +47,7 @@ struct VoiceNoteView: View {
Image(systemName: playback.isPlaying ? "pause.fill" : "play.fill")
.foregroundColor(.white)
.frame(width: 36, height: 36)
.background(Circle().fill(Color.green))
.background(Circle().fill(palette.accent))
}
.buttonStyle(.plain)
@@ -61,7 +62,7 @@ struct VoiceNoteView: View {
)
Text(playbackLabel)
.font(.bitchatSystem(size: 13, design: .monospaced))
.bitchatFont(size: 13)
.foregroundColor(Color.secondary)
if let onCancel = onCancel, isSending {
+3 -2
View File
@@ -6,6 +6,7 @@ struct WaveformView: View {
let sendProgress: Double?
let onSeek: ((Double) -> Void)?
let isInteractive: Bool
@ThemedPalette private var palette
private var clampedPlayback: Double {
max(0, min(1, playbackProgress))
@@ -37,9 +38,9 @@ struct WaveformView: View {
let binPosition = Double(index) / Double(samples.count)
let color: Color
if binPosition <= clampedPlayback {
color = Color.green
color = palette.accent
} else if let send = clampedSend, binPosition <= send {
color = Color.blue
color = palette.accentBlue
} else {
color = Color.gray.opacity(0.35)
}
+7 -8
View File
@@ -3,8 +3,7 @@ import BitFoundation
struct MeshPeerList: View {
@EnvironmentObject private var peerListModel: PeerListModel
let textColor: Color
let secondaryTextColor: Color
@ThemedPalette private var palette
let onTapPeer: (PeerID) -> Void
let onToggleFavorite: (PeerID) -> Void
let onShowFingerprint: (PeerID) -> Void
@@ -28,8 +27,8 @@ struct MeshPeerList: View {
if peerListModel.meshRows.isEmpty {
VStack(alignment: .leading, spacing: 0) {
Text(Strings.noneNearby)
.font(.bitchatSystem(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
.bitchatFont(size: 14)
.foregroundColor(palette.secondary)
.padding(.horizontal)
.padding(.top, 12)
}
@@ -64,18 +63,18 @@ struct MeshPeerList: View {
// Fallback icon for others (dimmed)
Image(systemName: "person")
.font(.bitchatSystem(size: 10))
.foregroundColor(secondaryTextColor)
.foregroundColor(palette.secondary)
}
let (base, suffix) = peer.displayName.splitSuffix()
HStack(spacing: 0) {
Text(base)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(baseColor)
if !suffix.isEmpty {
let suffixColor = isMe ? Color.orange.opacity(0.6) : baseColor.opacity(0.6)
Text(suffix)
.font(.bitchatSystem(size: 14, design: .monospaced))
.bitchatFont(size: 14)
.foregroundColor(suffixColor)
}
}
@@ -123,7 +122,7 @@ struct MeshPeerList: View {
Button(action: { onToggleFavorite(peer.peerID) }) {
Image(systemName: peer.isFavorite ? "star.fill" : "star")
.font(.bitchatSystem(size: 12))
.foregroundColor(peer.isFavorite ? .yellow : secondaryTextColor)
.foregroundColor(peer.isFavorite ? .yellow : palette.secondary)
}
.buttonStyle(.plain)
}
+2 -1
View File
@@ -21,6 +21,7 @@ struct MessageListView: View {
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.appTheme) private var theme
let privatePeer: PeerID?
@Binding var isAtBottom: Bool
@@ -222,7 +223,7 @@ private extension MessageListView {
@ViewBuilder
func systemMessageRow(_ message: BitchatMessage) -> some View {
Text(conversationUIModel.formatMessage(message, colorScheme: colorScheme))
Text(conversationUIModel.formatMessage(message, colorScheme: colorScheme, theme: theme))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
+13 -13
View File
@@ -21,7 +21,7 @@ struct MyQRView: View {
var body: some View {
VStack(spacing: 12) {
Text(Strings.title)
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
VStack(spacing: 10) {
QRCodeImage(data: qrString, size: 240)
@@ -29,7 +29,7 @@ struct MyQRView: View {
// Non-scrolling, fully visible URL (wraps across lines)
Text(qrString)
.font(.bitchatSystem(size: 11, design: .monospaced))
.bitchatFont(size: 11)
.textSelection(.enabled)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
@@ -69,7 +69,7 @@ struct QRCodeImage: View {
.frame(width: size, height: size)
.overlay(
Text(Strings.unavailable)
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
.foregroundColor(.gray)
)
}
@@ -150,7 +150,7 @@ struct QRScanView: View {
.clipShape(RoundedRectangle(cornerRadius: 8))
#else
Text(Strings.pastePrompt)
.font(.bitchatSystem(size: 14, weight: .medium, design: .monospaced))
.bitchatFont(size: 14, weight: .medium)
TextEditor(text: $input)
.frame(height: 100)
.border(Color.gray.opacity(0.4))
@@ -281,10 +281,10 @@ struct VerificationSheetView: View {
@EnvironmentObject private var verificationModel: VerificationModel
@Binding var isPresented: Bool
@State private var showingScanner = false
@Environment(\.colorScheme) var colorScheme
@ThemedPalette private var palette
private var backgroundColor: Color { colorScheme == .dark ? Color.black : Color.white }
private var accentColor: Color { colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) }
private var backgroundColor: Color { palette.background }
private var accentColor: Color { palette.accent }
private var boxColor: Color { Color.gray.opacity(0.1) }
var body: some View {
@@ -292,7 +292,7 @@ struct VerificationSheetView: View {
// Top header (always at top)
HStack {
Text("verification.sheet.title")
.font(.bitchatSystem(size: 14, weight: .bold, design: .monospaced))
.bitchatFont(size: 14, weight: .bold)
.foregroundColor(accentColor)
Spacer()
Button(action: {
@@ -316,7 +316,7 @@ struct VerificationSheetView: View {
if showingScanner {
VStack(alignment: .leading, spacing: 12) {
Text("verification.scan.prompt_friend")
.font(.bitchatSystem(size: 16, weight: .bold, design: .monospaced))
.bitchatFont(size: 16, weight: .bold)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
.foregroundColor(accentColor)
@@ -350,13 +350,13 @@ struct VerificationSheetView: View {
if showingScanner {
Button(action: { showingScanner = false }) {
Label("show my qr", systemImage: "qrcode")
.font(.bitchatSystem(size: 13, design: .monospaced))
.bitchatFont(size: 13)
}
.buttonStyle(.bordered)
} else {
Button(action: { showingScanner = true }) {
Label("scan someone else's qr", systemImage: "camera.viewfinder")
.font(.bitchatSystem(size: 13, weight: .medium, design: .monospaced))
.bitchatFont(size: 13, weight: .medium)
}
.buttonStyle(.bordered)
.tint(.gray)
@@ -367,7 +367,7 @@ struct VerificationSheetView: View {
verificationModel.isVerified(peerID: peerID) {
Button(action: { verificationModel.unverifyFingerprint(for: peerID) }) {
Label("remove verification", systemImage: "minus.circle")
.font(.bitchatSystem(size: 12, design: .monospaced))
.bitchatFont(size: 12)
}
.buttonStyle(.bordered)
.tint(.gray)
@@ -376,7 +376,7 @@ struct VerificationSheetView: View {
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
}
.background(backgroundColor)
.themedSheetBackground()
.onDisappear { showingScanner = false }
}
}