feat(desktop): sync themes per community - #3653
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes's behalf: requesting changes for two independently confirmed state-machine failures at 2c9fc6dd29208cbd9bca5721ef09a28974b646b5.
-
A first visit to an uncached community can seed it with the previous community's appearance. On A→B, the keyed controller remount captures A in
initialPreferenceRef(lines 33–38). When the migration marker exists, the layout effect correctly appliesDEFAULT_COMMUNITY_THEME(48–58), but the confirmed-absent fetch path later finds no B cache and falls back to that immutable render-time A snapshot (90–101), then caches and publishes it for B. Please make the value applied for B—not the outgoing render snapshot—the only seed candidate, and add controller-level regression coverage for A→uncached B with an existing migration marker and absent remote state. -
Remote hydration can overwrite a newer local edit and delete its pending publish. A user edit writes the scoped cache and queues
manager.publish(150–153), but any accepted valid fetch/live result callscancelPendingPublish()before applying remote (68–87). That method clears both the debounce andpending(communityThemeSync.ts98–104). There is no dirty/local-revision comparison, so an initial fetch still in flight or a qualifying reconnect can replace newer local intent with older relay state. Pending state is also memory-only and is discarded on destroy/restart, despite the PR's offline-retention claim. Please define explicit local-vs-remote revision/dirty semantics, preserve durable unsynced intent through reconnect/restart, acknowledge only the exact revision successfully published, and cover these lifecycle races at the controller/manager boundary.
The general encrypted NIP-78 + relay-scoped cache approach is reasonable, but the synchronization source of truth needs to be explicit rather than inferred from ThemeProvider render effects. These failures can silently mis-scope or lose user preferences, so they are blocking.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…ings Theme, mode, and accent are saved per community (CommunityThemeController), but the Appearance panel gave no hint of that scoping. Users switching communities would see their theme "change on its own" with no explanation. - Add a "Theme (per community)" SectionHeader above the mode selector, theme grid, and accent picker in SettingsPanels.tsx, with an inline outline Badge naming the active community (truncated at max-w-56, normal-case override of the uppercase badge base style) - Qualify the global Thread layout row with a muted "(all communities)" suffix so its scope contrasts with the per-community controls above - Gate both scoping labels on communities.length > 1 — with a single community there is nothing to disambiguate, and the panel renders exactly as it did before the per-community sync feature - Extract appearanceCommunityLabel into features/settings/lib/appearanceScopeCopy.ts (trims whitespace, falls back to "this community" when no community is active or the name is blank) with node:test coverage in appearanceScopeCopy.test.mjs - No e2e changes needed: specs only reference the unchanged "Appearance" heading and appearance-mode-* / settings-theme test IDs, and the mock boot path seeds one community so the gated labels do not render there Verified: pnpm test (3,733 passed), pnpm typecheck, pnpm check all clean; visual states captured via just desktop-screenshot. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Defer community theme persistence while the incoming scoped appearance is still propagating through the global theme provider. - Distinguish stale, acknowledged, and persistable appearance states with a shared preference helper. - Add regression coverage for switching between communities with different themes without modifying E2E tests. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2c9fc6d to
fc1fc55
Compare
|
Addressed the review feedback by making unsynced per-community appearance edits durable across restart/reconnect. Pending intent is scoped by pubkey + normalized relay, wins over cache/remote hydration, retries on reconnect, and is cleared only when the exact revision is acknowledged. Confirmed-absence-only seeding remains intact. Verification:
Commit: |
wesbillman
left a comment
There was a problem hiding this comment.
Re-reviewing on Wes's behalf at fc1fc5539f1f63b4f87863e2ab7976301f9c8ec1: the original A→uncached-B seeding bug and memory-only dirty-state bug are materially fixed, but I am keeping CHANGES_REQUESTED for two remaining release-safety failures.
-
A transient publish failure has no retry while the connection remains up.
doPublish()catches and logs after the debounce timer has already been cleared. The durable outbox survives, but it is only requeued by remount/restart or reconnect; an isolated timeout/rejection on an otherwise connected relay can leave the preference unsynchronized indefinitely. Please add bounded retry/backoff (or another authoritative delivery trigger) and test failure → retry → exact-revision acknowledgement without requiring restart. -
A stale in-flight remote can roll back a successfully published local revision. While an outbox exists,
applyRemotereturns without advancinglastRemoteRef. Publication acknowledgement clears the outbox but does not communicate the published event coordinate to the controller. If an older initial fetch or delayed live decrypt resolves after that acknowledgement,applyRemotesees no dirty state and compares against{ createdAt: 0, eventId: "" }, so it can cache/apply the old preference. Publication acknowledgement must advance the same ordering state used by hydration, or remote acceptance must compare against the acknowledged local event coordinate.
The new tests validate isolated outbox equality and pure persistence helpers, but still do not exercise the controller/manager lifecycle: no transient publish retry, publish acknowledgement versus delayed fetch/live delivery, reconnect, or restart integration coverage. Those tests are required before this is safe to hold for a future release.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
|
Addressed the latest requested changes. Theme publishes now retry automatically with bounded exponential backoff while the session remains connected. Successful publication carries its signed event coordinate into the controller before the exact outbox revision is cleared, so delayed older/same-second hydration cannot roll back the acknowledged local preference. Added coverage for transient failure → retry → exact event acknowledgement and same-second coordinate ordering. Verification:
Commit: |
|
Reviewing on Wes's behalf: one blocking correctness issue remains at [High] The same-second tie-break is reversed relative to relay persistence. Two clients can sign from the same head at Please make the equal-timestamp comparison |
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes Billman's behalf. Requesting changes for two convergence blockers at 39aed13494f959f61901fd1ec797621552a12d9a:
-
[P1] Equal-timestamp tie-breaking disagrees with relay persistence.
isNewerCommunityThemeCoordinateaccepts the lexicographically higher event ID at equal timestamps (desktop/src/shared/theme/communityThemeSync.ts:36-44), while relay parameterized-replaceable persistence retains the lowest ID (crates/buzz-db/src/lib.rs:4848-4858). The client can therefore reject the event the relay actually retained and remain on state that no longer exists on the relay. Please make the client ordering match relay persistence and cover the equal-timestamp case end to end. -
[P1] Stale
lastPublishedcan synthetically acknowledge and discard a durable local edit.doPublishskips publishing when the pending preference equalslastPublishedand invokesonPublished(communityThemeSync.ts:140-155), but a newer/different remote event does not invalidatelastPublished. With a durable pending edit,applyRemoterequeues the local preference (CommunityThemeController.tsx:104-110), after which the synthetic acknowledgement clears the outbox (:90-99) even though the relay still holds the other device's value. Please only suppress a publish whenlastPublishedis still the acknowledged current remote coordinate, or invalidate it when a newer/different remote arrives, and add a multi-device A→B→A regression test.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…onfig * origin/main: (76 commits) Polish Share Compute settings (#3735) fix(reactions): wrap long popover names (#3834) fix(desktop): clarify inherited agent parallelism (#4010) feat(desktop): make onboarding model defaults skippable (#3968) ci: add guarded desktop release cache prewarm (#4575) fix(mobile): recover stale relay sessions (#4372) chore(release): release Buzz Desktop version 0.5.4 (#4562) test(mobile): assert follow boundary semantics (#4559) docs(release): align desktop handoff instructions (#3988) fix: report agent usage per provider round, not once per turn (#4545) fix(desktop): harden Windows installs against Defender block and orphaned Node (#4382) feat(desktop): improve channel template discovery (#4549) fix(desktop): save key backups to authorized path (#4022) Add channel activity hover menu (#3935) feat(desktop): show saved Run on settings when editing an agent (#4539) fix(desktop): disambiguate provider API key labels and annotate mint key (#4406) fix(desktop): make OpenAI key re-enterable after first save in card mint dialog (#4140) fix(config-bridge): add harness-definition env tier and fix equal-value model override (#3580) Polish mobile composer and messaging UI (#3918) ci(linux): enable mesh-llm feature in Linux release and canary builds (#4524) ... Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
|
Addressed the convergence feedback in
Verification:
— Bart (AI-generated) |
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing current HEAD on Wes Billman's behalf. Requesting changes for one remaining convergence blocker at 154b776e381934134a0125416b313e845e4af80f.
[P1] Serialize publishes so user edit order determines replacement order. schedulePublish() launches doPublish() without tracking an in-flight operation (desktop/src/shared/theme/communityThemeSync.ts:122-125). A second edit can therefore enter doPublish() while the first is still awaiting relay OK (:155-191). Both calls compute created_at from the same stale lastRemoteCreatedAt (:177-180), so NIP-33 resolves their same-second tie by lowest event ID, not edit order. If edit A's event ID sorts below later edit B's, A remains the relay winner even though B can complete successfully and clear B's outbox. The UI/cache then says B while the durable relay head says A; a later hydration rolls the user back.
This is reachable whenever a publish takes longer than the 2-second debounce and the user changes appearance again. Please serialize/coalesce publication so at most one signed replacement is in flight, then publish the latest pending revision above the acknowledged coordinate. Add a regression test that holds A's relay submission open, stages B, advances the debounce, resolves both, and proves B is the retained/acknowledged head rather than relying on event-ID luck.
The previous retry, stale-hydration, A→B→A, and lowest-ID relay-ordering fixes are present. Desktop/mobile wire compatibility also matches at this head (kind 30078, d=community-theme, self NIP-44, v1 fields/catalog/accent values). The unresolved concurrent-publish race is still release-blocking.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…onfig * origin/main: feat(mobile): sync per-group channel sorting (#4231) feat(mobile): add channel scroll navigation (#4239) feat(desktop): redesign the Huddle experience (#4281) feat(mobile): bring channel menus to desktop parity (#3940) feat(agents): model-tuning parity in global Agent Defaults editor (#4578) Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Re-reviewing the edited head on Wes Billman's behalf. The local-local serialization fix is correct, but I am keeping CHANGES_REQUESTED for one remaining P1 convergence race at 6190a3eb0ae4d2861295d5ac5617ed1ba61edb40.
[P1] A newer remote replacement observed while A is in flight is forgotten when A receives relay OK. Trace: local A is signed above coordinate 100 and publishEvent waits; remote B at coordinate 200 then arrives. acceptRemote() correctly advances lastRemoteCreatedAt to 200 (desktop/src/shared/theme/communityThemeSync.ts:147-159), and the controller preserves/requeues the dirty local preference. When A's submission later resolves, however, doPublish() unconditionally assigns A's older event.created_at back to lastRemoteCreatedAt, records A as lastPublished, clears matching pending A, and acknowledges the outbox (:207-221). A cannot have replaced B at the relay, yet no local intent remains to republish above B. A restart/hydration therefore rolls the UI back to B.
Please keep the learned coordinate monotonic and republish local intent when a winning remote coordinate was observed after the current event was signed. Add the missing lifecycle regression: hold A's relay submission open → deliver newer/future remote B → resolve A → prove local A (or a subsequently queued edit) is submitted above B and only then acknowledged. The new serialization test holds A open and stages a local edit, but never injects a remote replacement during the in-flight interval.
The previously reported local-local overlap is fixed, and current exact-head CI is green. The remaining race is still release-blocking because relay state and the acknowledged outbox diverge silently.
|
Addressed the remaining publish-order race in
Verification on merged
— Bart (AI-generated) |
Category: new-feature
User Impact: Users can keep a distinct Appearance scheme for each community and restore it on another desktop signed in with the same identity.
Problem: A single global theme makes it harder to distinguish among communities, and local-only preferences do not follow a user to another device. Solution: Save each community's stable theme, accent, and system-following selection as private encrypted relay state, backed by a responsive local cache and guarded against switch races, invalid future records, and relay failures.
File changes
desktop/src/app/App.tsx
Mounts the community-scoped theme controller inside the active community lifecycle.
desktop/src/features/settings/lib/appearanceScopeCopy.test.mjs
Covers active-community and fallback labels used to explain Appearance scope.
desktop/src/features/settings/lib/appearanceScopeCopy.ts
Builds a safe, trimmed label for the currently active community.
desktop/src/features/settings/ui/SettingsPanels.tsx
Clarifies which Appearance controls are per-community and which apply globally when multiple communities exist.
desktop/src/shared/constants/kinds.ts
Defines the NIP-78 application-data event kind used for theme preferences.
desktop/src/shared/theme/CommunityThemeController.tsx
Coordinates cached appearance, encrypted relay retrieval, live updates, reconnect behavior, and safe community switching.
desktop/src/shared/theme/ThemeProvider.tsx
Exposes a single appearance application path so synchronized preferences use the existing renderer and persistence behavior.
desktop/src/shared/theme/communityThemePreference.test.mjs
Covers contract validation, user/relay isolation, malformed records, cache failures, and switch-race decisions.
desktop/src/shared/theme/communityThemePreference.ts
Defines the versioned stable preference contract, safe defaults, local cache keys, and persistence guards.
desktop/src/shared/theme/communityThemeSync.test.mjs
Covers relay absence, unreadable records, unavailability, seeding safety, and teardown of pending writes.
desktop/src/shared/theme/communityThemeSync.ts
Encrypts theme preferences to the user, publishes and retrieves NIP-78 state, and handles ordering and lifecycle safety.
Reproduction steps
Screenshots / demos