* 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>
Feature models observe per-conversation objects directly: PublicChatModel
forwards the active Conversation's objectWillChange, PrivateInboxModel
republishes only for the selected peer's conversation - background
appends no longer invalidate foreground views. LegacyConversationStore,
the coalescing bridge, and IdentityResolver are deleted (resolver
canonicalization proved display-invisible: nothing enumerates direct
conversations, lookups are by exact peer ID, and raw keying is strictly
more robust - documented as a design deviation). Selection state moves
into the store. ChatViewModel.messages/privateChats survive as derived
read views for coordinators that genuinely need them; hot paths use a
new store-direct privateMessages(for:) witness.
Final numbers vs pre-migration baselines:
pipeline.privateIngest 9.7k -> 24.0k msg/s (2.5x)
pipeline.publicIngest 6.8k -> 13.7k msg/s (2.0x)
delivery updates 38k -> 117-133k/s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-conversation ObservableObjects with O(1) dedup via an incrementally
maintained message-ID index, binary-search timestamp insertion, folded
cap policies, a no-downgrade delivery rule, and a typed change subject.
All mutation flows through store intents (conversation mutators are
fileprivate). The previous store is renamed LegacyConversationStore
pending deletion in step 5. 16 behavioral tests including per-
conversation publish isolation; store.append benchmarks at ~144k
messages/sec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>