mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 16:25:23 +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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user