mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
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:
co-authored by
jack
Claude Fable 5
parent
c8381737bb
commit
af954b05ea
@@ -41,6 +41,10 @@ final class ConversationUIModel: ObservableObject {
|
||||
chatViewModel.currentColorScheme = colorScheme
|
||||
}
|
||||
|
||||
func setCurrentTheme(_ theme: AppTheme) {
|
||||
chatViewModel.currentTheme = theme
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String) {
|
||||
chatViewModel.sendMessage(message)
|
||||
}
|
||||
@@ -76,12 +80,12 @@ final class ConversationUIModel: ObservableObject {
|
||||
chatViewModel.completeNickname(nickname, in: &text)
|
||||
}
|
||||
|
||||
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
chatViewModel.formatMessageAsText(message, colorScheme: colorScheme)
|
||||
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme, theme: AppTheme? = nil) -> AttributedString {
|
||||
chatViewModel.formatMessageAsText(message, colorScheme: colorScheme, theme: theme)
|
||||
}
|
||||
|
||||
func formatMessageHeader(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
chatViewModel.formatMessageHeader(message, colorScheme: colorScheme)
|
||||
func formatMessageHeader(_ message: BitchatMessage, colorScheme: ColorScheme, theme: AppTheme? = nil) -> AttributedString {
|
||||
chatViewModel.formatMessageHeader(message, colorScheme: colorScheme, theme: theme)
|
||||
}
|
||||
|
||||
func mediaAttachment(for message: BitchatMessage) -> BitchatMessage.Media? {
|
||||
|
||||
@@ -15,6 +15,7 @@ struct BitchatApp: App {
|
||||
static let groupID = "group.\(bundleID)"
|
||||
|
||||
@StateObject private var runtime: AppRuntime
|
||||
@AppStorage(AppTheme.storageKey) private var appThemeRawValue = AppTheme.matrix.rawValue
|
||||
#if os(iOS)
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@@ -30,6 +31,7 @@ struct BitchatApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(\.appTheme, AppTheme(rawValue: appThemeRawValue) ?? .matrix)
|
||||
.environmentObject(runtime.publicChatModel)
|
||||
.environmentObject(runtime.privateInboxModel)
|
||||
.environmentObject(runtime.privateConversationModel)
|
||||
|
||||
@@ -540,6 +540,543 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"app_info.appearance.liquid_glass" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "زجاج سائل"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "লিকুইড গ্লাস"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "זכוכית נוזלית"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "लिक्विड ग्लास"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "リキッドガラス"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "리퀴드 글래스"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "लिक्विड ग्लास"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "жидкое стекло"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "லிக்விட் கிளாஸ்"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ลิควิดกลาส"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "рідке скло"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "لیکویڈ گلاس"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "liquid glass"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "液态玻璃"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "液態玻璃"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app_info.appearance.matrix" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ماتريكس"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ম্যাট্রিক্স"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "מטריקס"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "मैट्रिक्स"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "マトリックス"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "매트릭스"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "म्याट्रिक्स"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "матрица"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "மேட்ரிக்ஸ்"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "เมทริกซ์"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "матриця"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "میٹرکس"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "matrix"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "矩阵"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "矩陣"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app_info.appearance.title" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "المظهر"
|
||||
}
|
||||
},
|
||||
"bn" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "চেহারা"
|
||||
}
|
||||
},
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ERSCHEINUNGSBILD"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "APPEARANCE"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "APARIENCIA"
|
||||
}
|
||||
},
|
||||
"fil" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "HITSURA"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "APPARENCE"
|
||||
}
|
||||
},
|
||||
"he" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "מראה"
|
||||
}
|
||||
},
|
||||
"hi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "दिखावट"
|
||||
}
|
||||
},
|
||||
"id" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "TAMPILAN"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ASPETTO"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "外観"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "화면 모드"
|
||||
}
|
||||
},
|
||||
"ms" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "PENAMPILAN"
|
||||
}
|
||||
},
|
||||
"ne" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "रूप"
|
||||
}
|
||||
},
|
||||
"nl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "WEERGAVE"
|
||||
}
|
||||
},
|
||||
"pl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "WYGLĄD"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "APARÊNCIA"
|
||||
}
|
||||
},
|
||||
"pt-BR" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "APARÊNCIA"
|
||||
}
|
||||
},
|
||||
"ru" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ОФОРМЛЕНИЕ"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "UTSEENDE"
|
||||
}
|
||||
},
|
||||
"ta" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "தோற்றம்"
|
||||
}
|
||||
},
|
||||
"th" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ลักษณะที่ปรากฏ"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "GÖRÜNÜM"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ОФОРМЛЕННЯ"
|
||||
}
|
||||
},
|
||||
"ur" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ظاہری شکل"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "GIAO DIỆN"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "外观"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "外觀"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app_info.close" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
//
|
||||
// Theme.swift
|
||||
// bitchat
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A user-selectable app-wide visual theme. Persisted by raw value.
|
||||
enum AppTheme: String, CaseIterable, Identifiable {
|
||||
case matrix
|
||||
case liquidGlass
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// UserDefaults key backing the theme selection.
|
||||
static let storageKey = "appTheme"
|
||||
|
||||
var displayNameKey: LocalizedStringKey {
|
||||
switch self {
|
||||
case .matrix: return "app_info.appearance.matrix"
|
||||
case .liquidGlass: return "app_info.appearance.liquid_glass"
|
||||
}
|
||||
}
|
||||
|
||||
/// Font design used for themed text. Matrix keeps the terminal monospace;
|
||||
/// liquid glass uses the system default.
|
||||
var bodyFontDesign: Font.Design {
|
||||
switch self {
|
||||
case .matrix: return .monospaced
|
||||
case .liquidGlass: return .default
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether chrome surfaces (header/composer bars, input field) render as
|
||||
/// translucent glass/material instead of the flat matrix background.
|
||||
var usesGlassChrome: Bool {
|
||||
self == .liquidGlass
|
||||
}
|
||||
|
||||
/// Discriminator mixed into per-message formatting caches so cached
|
||||
/// AttributedStrings from one theme are never served under another.
|
||||
/// Empty for matrix to keep its historical cache keys.
|
||||
var formatCacheVariant: String {
|
||||
switch self {
|
||||
case .matrix: return ""
|
||||
case .liquidGlass: return "lg:"
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the semantic color palette for this theme under the given color scheme.
|
||||
func palette(for colorScheme: ColorScheme) -> ThemePalette {
|
||||
switch self {
|
||||
case .matrix:
|
||||
return .matrix(colorScheme)
|
||||
case .liquidGlass:
|
||||
return .liquidGlass(colorScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Semantic colors for the active theme, resolved against the current color scheme.
|
||||
/// Views should consume these via `@ThemedPalette` rather than computing colors inline.
|
||||
struct ThemePalette {
|
||||
/// Primary window/sheet background.
|
||||
let background: Color
|
||||
/// Primary text color.
|
||||
let primary: Color
|
||||
/// De-emphasized text (timestamps, hints, captions).
|
||||
let secondary: Color
|
||||
/// Interactive tint (buttons, toggles, selection).
|
||||
let accent: Color
|
||||
/// Location/geohash channel accent (badges, counts, subtitles).
|
||||
let locationAccent: Color
|
||||
/// Informational accent (links, read receipts, teleport markers).
|
||||
let accentBlue: Color
|
||||
/// Destructive/error accent.
|
||||
let alertRed: Color
|
||||
/// Hairline separators.
|
||||
let divider: Color
|
||||
|
||||
static func matrix(_ colorScheme: ColorScheme) -> ThemePalette {
|
||||
let isDark = colorScheme == .dark
|
||||
let green = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
return ThemePalette(
|
||||
background: isDark ? Color.black : Color.white,
|
||||
primary: green,
|
||||
secondary: green.opacity(0.8),
|
||||
accent: green,
|
||||
locationAccent: green,
|
||||
accentBlue: Color(red: 0.0, green: 0.478, blue: 1.0),
|
||||
alertRed: Color(red: 0.75, green: 0.1, blue: 0.1),
|
||||
divider: isDark ? Color.white.opacity(0.12) : Color.black.opacity(0.08)
|
||||
)
|
||||
}
|
||||
|
||||
static func liquidGlass(_ colorScheme: ColorScheme) -> ThemePalette {
|
||||
ThemePalette(
|
||||
background: systemBackground,
|
||||
primary: .primary,
|
||||
secondary: .secondary,
|
||||
accent: .blue,
|
||||
locationAccent: .green,
|
||||
accentBlue: .blue,
|
||||
alertRed: .red,
|
||||
divider: separator
|
||||
)
|
||||
}
|
||||
|
||||
private static var systemBackground: Color {
|
||||
#if os(iOS)
|
||||
Color(UIColor.systemBackground)
|
||||
#else
|
||||
Color(NSColor.windowBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static var separator: Color {
|
||||
#if os(iOS)
|
||||
Color(UIColor.separator)
|
||||
#else
|
||||
Color(NSColor.separatorColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppThemeKey: EnvironmentKey {
|
||||
static let defaultValue: AppTheme = .matrix
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var appTheme: AppTheme {
|
||||
get { self[AppThemeKey.self] }
|
||||
set { self[AppThemeKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the active theme's palette against the view's color scheme.
|
||||
///
|
||||
/// @ThemedPalette private var palette
|
||||
/// var body: some View { Text("hi").foregroundColor(palette.primary) }
|
||||
@propertyWrapper
|
||||
struct ThemedPalette: DynamicProperty {
|
||||
@Environment(\.appTheme) private var theme
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var wrappedValue: ThemePalette { theme.palette(for: colorScheme) }
|
||||
}
|
||||
|
||||
// MARK: - Themed view helpers
|
||||
|
||||
/// Themed replacement for `.font(.bitchatSystem(size:weight:design: .monospaced))`:
|
||||
/// monospaced under matrix, system default under liquid glass.
|
||||
private struct ThemedFontModifier: ViewModifier {
|
||||
@Environment(\.appTheme) private var theme
|
||||
let size: CGFloat
|
||||
let weight: Font.Weight
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.font(.bitchatSystem(size: size, weight: weight, design: theme.bodyFontDesign))
|
||||
}
|
||||
}
|
||||
|
||||
/// Root backdrop. Matrix gets its flat background; glass gets a subtle static
|
||||
/// gradient with a soft tinted glow — glass panels need visual texture behind
|
||||
/// them to refract, and collapse to flat gray over a solid color.
|
||||
struct ThemedRootBackground: View {
|
||||
@Environment(\.appTheme) private var theme
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ThemedPalette private var palette
|
||||
|
||||
var body: some View {
|
||||
if theme.usesGlassChrome {
|
||||
let isDark = colorScheme == .dark
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: isDark
|
||||
? [Color(red: 0.09, green: 0.10, blue: 0.15), Color(red: 0.04, green: 0.04, blue: 0.07)]
|
||||
: [Color(red: 0.93, green: 0.95, blue: 1.0), Color(red: 0.98, green: 0.97, blue: 0.99)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
RadialGradient(
|
||||
colors: [Color.blue.opacity(isDark ? 0.22 : 0.12), .clear],
|
||||
center: .topLeading,
|
||||
startRadius: 0,
|
||||
endRadius: 600
|
||||
)
|
||||
RadialGradient(
|
||||
colors: [Color.purple.opacity(isDark ? 0.14 : 0.08), .clear],
|
||||
center: .bottomTrailing,
|
||||
startRadius: 0,
|
||||
endRadius: 500
|
||||
)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
} else {
|
||||
palette.background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps glass-shape content in real Liquid Glass on OS 26+, with a material
|
||||
/// fallback below that keeps the frosted look.
|
||||
private struct GlassPanel<S: Shape>: ViewModifier {
|
||||
let shape: S
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
#if compiler(>=6.2)
|
||||
if #available(iOS 26.0, macOS 26.0, *) {
|
||||
content.glassEffect(.regular, in: shape)
|
||||
} else {
|
||||
materialFallback(content)
|
||||
}
|
||||
#else
|
||||
materialFallback(content)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func materialFallback(_ content: Content) -> some View {
|
||||
content
|
||||
.background(shape.fill(.ultraThinMaterial))
|
||||
.overlay(shape.stroke(Color.white.opacity(0.15), lineWidth: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
/// Chrome surface for the header and composer. Matrix keeps the original flat
|
||||
/// edge-to-edge wash; glass floats the content as an inset Liquid Glass panel
|
||||
/// (content is expected to scroll underneath via safe-area insets).
|
||||
private struct ThemedChromePanelModifier: ViewModifier {
|
||||
@Environment(\.appTheme) private var theme
|
||||
@ThemedPalette private var palette
|
||||
let edge: VerticalEdge
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if theme.usesGlassChrome {
|
||||
content
|
||||
.modifier(GlassPanel(shape: RoundedRectangle(cornerRadius: 18, style: .continuous)))
|
||||
.padding(.horizontal, 8)
|
||||
.padding(edge == .top ? .top : .bottom, 4)
|
||||
} else {
|
||||
content.background(palette.background.opacity(0.95))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background for the composer input field. Matrix keeps its translucent fill;
|
||||
/// glass leaves it clear — the field sits inside the composer's glass panel,
|
||||
/// and glass cannot sample other glass.
|
||||
private struct ThemedInputBackgroundModifier: ViewModifier {
|
||||
@Environment(\.appTheme) private var theme
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private var shape: RoundedRectangle {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if theme.usesGlassChrome {
|
||||
content
|
||||
} else {
|
||||
content.background(
|
||||
shape.fill(colorScheme == .dark ? Color.black.opacity(0.35) : Color.white.opacity(0.7))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func bitchatFont(size: CGFloat, weight: Font.Weight = .regular) -> some View {
|
||||
modifier(ThemedFontModifier(size: size, weight: weight))
|
||||
}
|
||||
|
||||
func themedChromePanel(edge: VerticalEdge) -> some View {
|
||||
modifier(ThemedChromePanelModifier(edge: edge))
|
||||
}
|
||||
|
||||
func themedInputBackground() -> some View {
|
||||
modifier(ThemedInputBackgroundModifier())
|
||||
}
|
||||
|
||||
/// Floating surface for popover-style boxes (autocomplete, command
|
||||
/// suggestions): glass panel under liquid glass, the original flat
|
||||
/// background + hairline stroke under matrix.
|
||||
func themedOverlayPanel() -> some View {
|
||||
modifier(ThemedOverlayPanelModifier())
|
||||
}
|
||||
|
||||
/// Root background for sheets — same backdrop as the main window so every
|
||||
/// surface speaks one visual language.
|
||||
func themedSheetBackground() -> some View {
|
||||
background(ThemedRootBackground())
|
||||
}
|
||||
|
||||
/// Flat background wash for bars/headers inside sheets. Matrix keeps its
|
||||
/// opaque wash; glass goes transparent so the backdrop gradient shows.
|
||||
func themedSurface(opacity: Double = 1.0) -> some View {
|
||||
modifier(ThemedSurfaceModifier(opacity: opacity))
|
||||
}
|
||||
}
|
||||
|
||||
private struct ThemedSurfaceModifier: ViewModifier {
|
||||
@Environment(\.appTheme) private var theme
|
||||
@ThemedPalette private var palette
|
||||
let opacity: Double
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if theme.usesGlassChrome {
|
||||
content
|
||||
} else {
|
||||
content.background(palette.background.opacity(opacity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ThemedOverlayPanelModifier: ViewModifier {
|
||||
@Environment(\.appTheme) private var theme
|
||||
@ThemedPalette private var palette
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
if theme.usesGlassChrome {
|
||||
content.modifier(GlassPanel(shape: RoundedRectangle(cornerRadius: 12, style: .continuous)))
|
||||
} else {
|
||||
content
|
||||
.background(palette.background)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(palette.secondary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ final class ChatMessageFormatter {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme, theme: AppTheme = .matrix) -> AttributedString {
|
||||
let design = theme.bodyFontDesign
|
||||
let isSelf: Bool = {
|
||||
if let spid = message.senderPeerID {
|
||||
if case .location(let channel) = viewModel.activeChannel, spid.isGeoChat {
|
||||
@@ -40,7 +41,7 @@ final class ChatMessageFormatter {
|
||||
}()
|
||||
|
||||
let isDark = colorScheme == .dark
|
||||
if let cachedText = message.getCachedFormattedText(isDark: isDark, isSelf: isSelf) {
|
||||
if let cachedText = message.getCachedFormattedText(isDark: isDark, isSelf: isSelf, variant: theme.formatCacheVariant) {
|
||||
return cachedText
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ final class ChatMessageFormatter {
|
||||
var senderStyle = AttributeContainer()
|
||||
senderStyle.foregroundColor = baseColor
|
||||
let fontWeight: Font.Weight = isSelf ? .bold : .medium
|
||||
senderStyle.font = .bitchatSystem(size: 14, weight: fontWeight, design: .monospaced)
|
||||
senderStyle.font = .bitchatSystem(size: 14, weight: fontWeight, design: design)
|
||||
if let spid = message.senderPeerID,
|
||||
let url = URL(string: "bitchat://user/\(spid.toPercentEncoded())") {
|
||||
senderStyle.link = url
|
||||
@@ -79,8 +80,8 @@ final class ChatMessageFormatter {
|
||||
var plainStyle = AttributeContainer()
|
||||
plainStyle.foregroundColor = baseColor
|
||||
plainStyle.font = isSelf
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
|
||||
: .bitchatSystem(size: 14, design: .monospaced)
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: design)
|
||||
: .bitchatSystem(size: 14, design: design)
|
||||
result.append(AttributedString(content).mergingAttributes(plainStyle))
|
||||
} else {
|
||||
let hashtagRegex = Patterns.hashtag
|
||||
@@ -197,8 +198,8 @@ final class ChatMessageFormatter {
|
||||
var beforeStyle = AttributeContainer()
|
||||
beforeStyle.foregroundColor = baseColor
|
||||
beforeStyle.font = isSelf
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
|
||||
: .bitchatSystem(size: 14, design: .monospaced)
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: design)
|
||||
: .bitchatSystem(size: 14, design: design)
|
||||
if isMentioned {
|
||||
beforeStyle.font = beforeStyle.font?.bold()
|
||||
}
|
||||
@@ -230,7 +231,7 @@ final class ChatMessageFormatter {
|
||||
mentionStyle.font = .bitchatSystem(
|
||||
size: 14,
|
||||
weight: isSelf ? .bold : .semibold,
|
||||
design: .monospaced
|
||||
design: design
|
||||
)
|
||||
let mentionColor: Color = isMentionToMe ? .orange : baseColor
|
||||
mentionStyle.foregroundColor = mentionColor
|
||||
@@ -267,8 +268,8 @@ final class ChatMessageFormatter {
|
||||
|
||||
var tagStyle = AttributeContainer()
|
||||
tagStyle.font = isSelf
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
|
||||
: .bitchatSystem(size: 14, design: .monospaced)
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: design)
|
||||
: .bitchatSystem(size: 14, design: design)
|
||||
tagStyle.foregroundColor = baseColor
|
||||
if isGeohash && !attachedToMentionToken && standalone,
|
||||
let url = URL(string: "bitchat://geohash/\(token)") {
|
||||
@@ -280,15 +281,15 @@ final class ChatMessageFormatter {
|
||||
var spacer = AttributeContainer()
|
||||
spacer.foregroundColor = baseColor
|
||||
spacer.font = isSelf
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
|
||||
: .bitchatSystem(size: 14, design: .monospaced)
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: design)
|
||||
: .bitchatSystem(size: 14, design: design)
|
||||
result.append(AttributedString(" ").mergingAttributes(spacer))
|
||||
} else {
|
||||
var matchStyle = AttributeContainer()
|
||||
matchStyle.font = .bitchatSystem(
|
||||
size: 14,
|
||||
weight: isSelf ? .bold : .semibold,
|
||||
design: .monospaced
|
||||
design: design
|
||||
)
|
||||
if type == "url" {
|
||||
matchStyle.foregroundColor = isSelf ? .orange : .blue
|
||||
@@ -310,8 +311,8 @@ final class ChatMessageFormatter {
|
||||
var remainingStyle = AttributeContainer()
|
||||
remainingStyle.foregroundColor = baseColor
|
||||
remainingStyle.font = isSelf
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: .monospaced)
|
||||
: .bitchatSystem(size: 14, design: .monospaced)
|
||||
? .bitchatSystem(size: 14, weight: .bold, design: design)
|
||||
: .bitchatSystem(size: 14, design: design)
|
||||
if isMentioned {
|
||||
remainingStyle.font = remainingStyle.font?.bold()
|
||||
}
|
||||
@@ -322,27 +323,28 @@ final class ChatMessageFormatter {
|
||||
let timestamp = AttributedString(" [\(message.formattedTimestamp)]")
|
||||
var timestampStyle = AttributeContainer()
|
||||
timestampStyle.foregroundColor = Color.gray.opacity(0.7)
|
||||
timestampStyle.font = .bitchatSystem(size: 10, design: .monospaced)
|
||||
timestampStyle.font = .bitchatSystem(size: 10, design: design)
|
||||
result.append(timestamp.mergingAttributes(timestampStyle))
|
||||
} else {
|
||||
var contentStyle = AttributeContainer()
|
||||
contentStyle.foregroundColor = Color.gray
|
||||
let content = AttributedString("* \(message.content) *")
|
||||
contentStyle.font = .bitchatSystem(size: 12, design: .monospaced).italic()
|
||||
contentStyle.font = .bitchatSystem(size: 12, design: design).italic()
|
||||
result.append(content.mergingAttributes(contentStyle))
|
||||
|
||||
let timestamp = AttributedString(" [\(message.formattedTimestamp)]")
|
||||
var timestampStyle = AttributeContainer()
|
||||
timestampStyle.foregroundColor = Color.gray.opacity(0.5)
|
||||
timestampStyle.font = .bitchatSystem(size: 10, design: .monospaced)
|
||||
timestampStyle.font = .bitchatSystem(size: 10, design: design)
|
||||
result.append(timestamp.mergingAttributes(timestampStyle))
|
||||
}
|
||||
|
||||
message.setCachedFormattedText(result, isDark: isDark, isSelf: isSelf)
|
||||
message.setCachedFormattedText(result, isDark: isDark, isSelf: isSelf, variant: theme.formatCacheVariant)
|
||||
return result
|
||||
}
|
||||
|
||||
func formatMessageHeader(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
func formatMessageHeader(_ message: BitchatMessage, colorScheme: ColorScheme, theme: AppTheme = .matrix) -> AttributedString {
|
||||
let design = theme.bodyFontDesign
|
||||
let isSelf: Bool = {
|
||||
if let spid = message.senderPeerID {
|
||||
if case .location(let channel) = viewModel.activeChannel, spid.id.hasPrefix("nostr:"),
|
||||
@@ -362,7 +364,7 @@ final class ChatMessageFormatter {
|
||||
if message.sender == "system" {
|
||||
var style = AttributeContainer()
|
||||
style.foregroundColor = baseColor
|
||||
style.font = .bitchatSystem(size: 14, weight: .medium, design: .monospaced)
|
||||
style.font = .bitchatSystem(size: 14, weight: .medium, design: design)
|
||||
return AttributedString(message.sender).mergingAttributes(style)
|
||||
}
|
||||
|
||||
@@ -370,7 +372,7 @@ final class ChatMessageFormatter {
|
||||
let (baseName, suffix) = message.sender.splitSuffix()
|
||||
var senderStyle = AttributeContainer()
|
||||
senderStyle.foregroundColor = baseColor
|
||||
senderStyle.font = .bitchatSystem(size: 14, weight: isSelf ? .bold : .medium, design: .monospaced)
|
||||
senderStyle.font = .bitchatSystem(size: 14, weight: isSelf ? .bold : .medium, design: design)
|
||||
if let spid = message.senderPeerID,
|
||||
let url = URL(string: "bitchat://user/\(spid.id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? spid.id)") {
|
||||
senderStyle.link = url
|
||||
|
||||
@@ -139,6 +139,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
}
|
||||
private var visibleMessagesCache: [BitchatMessage]?
|
||||
@Published var currentColorScheme: ColorScheme = .light
|
||||
@Published var currentTheme: AppTheme = .matrix
|
||||
@Published var isConnected = false
|
||||
@Published var nickname: String = "" {
|
||||
didSet {
|
||||
@@ -1257,13 +1258,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
// MARK: - Message Formatting
|
||||
|
||||
@MainActor
|
||||
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
messageFormatter.formatMessageAsText(message, colorScheme: colorScheme)
|
||||
func formatMessageAsText(_ message: BitchatMessage, colorScheme: ColorScheme, theme: AppTheme? = nil) -> AttributedString {
|
||||
messageFormatter.formatMessageAsText(message, colorScheme: colorScheme, theme: theme ?? currentTheme)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func formatMessageHeader(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
|
||||
messageFormatter.formatMessageHeader(message, colorScheme: colorScheme)
|
||||
func formatMessageHeader(_ message: BitchatMessage, colorScheme: ColorScheme, theme: AppTheme? = nil) -> AttributedString {
|
||||
messageFormatter.formatMessageHeader(message, colorScheme: colorScheme, theme: theme ?? currentTheme)
|
||||
}
|
||||
|
||||
// MARK: - Noise Protocol Support
|
||||
|
||||
@@ -2,24 +2,24 @@ import SwiftUI
|
||||
|
||||
struct AppInfoView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ThemedPalette private var palette
|
||||
@AppStorage(AppTheme.storageKey) private var appThemeRawValue = AppTheme.matrix.rawValue
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .dark ? Color.black : Color.white
|
||||
private var selectedTheme: AppTheme {
|
||||
AppTheme(rawValue: appThemeRawValue) ?? .matrix
|
||||
}
|
||||
|
||||
private var textColor: Color {
|
||||
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
}
|
||||
private var backgroundColor: Color { palette.background }
|
||||
|
||||
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 }
|
||||
|
||||
// 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,11 +225,9 @@ struct AppInfoFeatureInfo {
|
||||
|
||||
struct SectionHeader: View {
|
||||
let title: LocalizedStringKey
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ThemedPalette private var palette
|
||||
|
||||
private var textColor: Color {
|
||||
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
}
|
||||
private var textColor: Color { palette.primary }
|
||||
|
||||
init(_ title: LocalizedStringKey) {
|
||||
self.title = title
|
||||
@@ -213,7 +235,7 @@ struct SectionHeader: View {
|
||||
|
||||
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,15 +243,11 @@ struct SectionHeader: View {
|
||||
|
||||
struct FeatureRow: View {
|
||||
let info: AppInfoFeatureInfo
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ThemedPalette private var palette
|
||||
|
||||
private var textColor: Color {
|
||||
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
}
|
||||
private var textColor: Color { palette.primary }
|
||||
|
||||
private var secondaryTextColor: Color {
|
||||
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
|
||||
}
|
||||
private var secondaryTextColor: Color { palette.secondary }
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
@@ -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 textColor: Color { palette.primary }
|
||||
|
||||
private var secondaryTextColor: Color {
|
||||
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
|
||||
@@ -13,15 +13,11 @@ struct FingerprintView: View {
|
||||
@EnvironmentObject private var verificationModel: VerificationModel
|
||||
let peerID: PeerID
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ThemedPalette private var palette
|
||||
|
||||
private var textColor: Color {
|
||||
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||
}
|
||||
private var textColor: Color { palette.primary }
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .dark ? Color.black : Color.white
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +142,6 @@ private struct ContentPeopleSheetHarness: View {
|
||||
isTextFieldFocused: $isTextFieldFocused,
|
||||
voiceRecordingVM: voiceRecordingVM,
|
||||
autocompleteDebounceTimer: $autocompleteDebounceTimer,
|
||||
backgroundColor: .black,
|
||||
textColor: .green,
|
||||
secondaryTextColor: .gray,
|
||||
headerHeight: 44,
|
||||
onSendMessage: {},
|
||||
showImagePicker: $showImagePicker,
|
||||
@@ -163,9 +160,6 @@ private struct ContentPeopleSheetHarness: View {
|
||||
isTextFieldFocused: $isTextFieldFocused,
|
||||
voiceRecordingVM: voiceRecordingVM,
|
||||
autocompleteDebounceTimer: $autocompleteDebounceTimer,
|
||||
backgroundColor: .black,
|
||||
textColor: .green,
|
||||
secondaryTextColor: .gray,
|
||||
headerHeight: 44,
|
||||
onSendMessage: {},
|
||||
showMacImagePicker: $showMacImagePicker
|
||||
@@ -335,8 +329,6 @@ struct ViewSmokeTests {
|
||||
|
||||
_ = mount(
|
||||
MeshPeerList(
|
||||
textColor: .green,
|
||||
secondaryTextColor: .gray,
|
||||
onTapPeer: { _ in },
|
||||
onToggleFavorite: { _ in },
|
||||
onShowFingerprint: { _ in }
|
||||
@@ -344,8 +336,6 @@ struct ViewSmokeTests {
|
||||
.environmentObject(featureModels.peerListModel)
|
||||
)
|
||||
_ = MeshPeerList(
|
||||
textColor: .green,
|
||||
secondaryTextColor: .gray,
|
||||
onTapPeer: { _ in },
|
||||
onToggleFavorite: { _ in },
|
||||
onShowFingerprint: { _ in }
|
||||
@@ -363,8 +353,6 @@ struct ViewSmokeTests {
|
||||
|
||||
_ = mount(
|
||||
MeshPeerList(
|
||||
textColor: .green,
|
||||
secondaryTextColor: .gray,
|
||||
onTapPeer: { _ in },
|
||||
onToggleFavorite: { _ in },
|
||||
onShowFingerprint: { _ in }
|
||||
@@ -389,10 +377,7 @@ struct ViewSmokeTests {
|
||||
messageText: Binding(
|
||||
get: { messageText },
|
||||
set: { messageText = $0 }
|
||||
),
|
||||
textColor: .green,
|
||||
backgroundColor: .black,
|
||||
secondaryTextColor: .gray
|
||||
)
|
||||
)
|
||||
.environmentObject(featureModels.privateConversationModel)
|
||||
.environmentObject(featureModels.locationChannelsModel)
|
||||
@@ -539,8 +524,6 @@ struct ViewSmokeTests {
|
||||
let (viewModel, _, _) = makeSmokeViewModel()
|
||||
let featureModels = makeSmokeFeatureModels(for: viewModel)
|
||||
let geohashPeopleList = GeohashPeopleList(
|
||||
textColor: .green,
|
||||
secondaryTextColor: .gray,
|
||||
onTapPerson: {}
|
||||
)
|
||||
let truncatableMessage = BitchatMessage(
|
||||
|
||||
@@ -34,12 +34,12 @@ public final class BitchatMessage: Codable {
|
||||
// Cached formatted text (not included in Codable)
|
||||
private var _cachedFormattedText: [String: AttributedString] = [:]
|
||||
|
||||
public func getCachedFormattedText(isDark: Bool, isSelf: Bool) -> AttributedString? {
|
||||
return _cachedFormattedText["\(isDark)-\(isSelf)"]
|
||||
public func getCachedFormattedText(isDark: Bool, isSelf: Bool, variant: String = "") -> AttributedString? {
|
||||
return _cachedFormattedText["\(variant)\(isDark)-\(isSelf)"]
|
||||
}
|
||||
|
||||
public func setCachedFormattedText(_ text: AttributedString, isDark: Bool, isSelf: Bool) {
|
||||
_cachedFormattedText["\(isDark)-\(isSelf)"] = text
|
||||
public func setCachedFormattedText(_ text: AttributedString, isDark: Bool, isSelf: Bool, variant: String = "") {
|
||||
_cachedFormattedText["\(variant)\(isDark)-\(isSelf)"] = text
|
||||
}
|
||||
|
||||
// Codable implementation
|
||||
|
||||
Reference in New Issue
Block a user