* 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>
The publish chain was healthy: the store mutates the shared
BitchatMessage and republishes, the .statusChanged fan-out reaches both
mirrored conversations, and PrivateInboxModel fires objectWillChange for
the selected DM under either key. The break was at the row view:
TextMessageView/MediaMessageView stored the reference-typed message and
read deliveryStatus in body, so SwiftUI's structural diff compared the
field by identity - same instance, mutated in place, row body skipped.
The blue tick waited for an unrelated invalidation (proven empirically
with a hosting-view probe).
Rows now snapshot deliveryStatus as a value at init; every republish
rebuilds row values with a fresh enum, the diff sees the change, and
the row re-renders immediately. Also fixes in-place send-progress
updates in media rows. Regression tests cover both mirrored selection
keyings at the feature-model level and the snapshot mechanic itself.
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>
All private-message mutations now flow through store intents:
coordinators, PrivateChatManager (its @Published dicts deleted - now
read-only views over the store), outbound sends, delivery status, and
chat migration. The O(1) store dedup replaces the full-scan duplicate
check; insertion order is maintained by the store so sanitizeChat's
re-sort is a documented no-op. Both bootstrapper Combine bridges and
the Task.yield store synchronization are deleted.
ChatViewModel.privateChats/unreadPrivateMessages become get-only derived
views (measured: naive rebuild equals a change-invalidated cache within
noise, so the simpler form stays). Feature models still read the legacy
store, fed by a coalescing LegacyConversationStoreBridge (one mirror
per burst, marked for step-5 deletion).
pipeline.privateIngest: 9.6k -> 14.7k msg/s (+53%).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Extract message list into dedicated MessageListView
* Fix broken smoke test
Mounts two distinct fixtures to cover separate render branches: a truncatable message (>2000 chars, no payment tokens) and a payment message (lightning + cashu, private, partial delivery). Expectations guard that each fixture actually reaches its intended branch.
---------
Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com>
* Extract voice-related code to a dedicated VM
* Minor optimization of permission request + fix ui bug
When isPreparing is being set to true before permission is granted, the UI is shown for a fraction of a second and it’s even worse if the permission is being asked constantly if the user drags the finger even when the alert is shown
* Remove dead code
* Remove unnecessary delegate conformance
* `actor VoiceRecorder` + other minor improvements
* Centralized opening system settings + open for mic access
* Better voice-recording state handling with Enum
* Remove manual timer management
* Simplify formatting + avoid jumping UI w/ countdown
* Add `requestingPermission` state to avoid repetitive calls
* Improve guarding of the states after awaits
* Fix potential “actor reentrancy across awaits” issue
* Remove short-circuiting on .idle + guard before error
* Fix playbackLabel’s formatting for duration vs remainder
---------
Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com>
* Expand coverage for transport, chat, and media flows
* Stabilize transport and media coverage tests
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>