Skip to content

fix(mobile): protect unpublished channel-section edits and sync channel sort#2829

Open
tellaho wants to merge 1 commit into
mainfrom
tho/mobile-sections-sync
Open

fix(mobile): protect unpublished channel-section edits and sync channel sort#2829
tellaho wants to merge 1 commit into
mainfrom
tho/mobile-sections-sync

Conversation

@tellaho

@tellaho tellaho commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Problem

Mobile channel-section edits were silently lost ("groups don't persist"). Investigation on main (ab3af8287) showed:

  1. The desktop channel-sections blob is on the relay and mobile decrypts/fetches/renders it correctly — the blank-sidebar screenshot was a stale build, not a read-path bug.
  2. The real regression is on the write path: sections publish on a 5s debounce, but ChannelSectionsNotifier.build() tears the manager down on every relay-session status flip without flushing (dispose(flushPending: false)). The rebuilt manager fetches the stale remote blob and unconditionally adopts it over newer local state — the just-made edit vanishes.
  3. Separately, mobile had zero channel-sort sync (channel-sort d-tag) — desktop-only since sync channel sort preferences across clients #1556.

Fix

Sections — dirty-state protection (mirrors the read-state manager pattern):

  • Persisted dirty-since flag in ChannelSectionsStorage so unpublished edits survive manager teardown and app restarts.
  • _mergeEvent never lets a remote blob overwrite unpublished local edits; the pending publish reconciles the relay (LWW timestamps still advance so our event sorts after the remote one).
  • Publish-on-reconnect: initialize() re-schedules a publish when the dirty flag is set, and seed-publishes local state when the relay confirms it has no blob (never on a failed fetch).
  • Edit-generation guard so a publish only clears the dirty flag when no new edit landed mid-flight.

Channel-sort parity (new mobile feature):

  • channel_sort/ storage/manager/provider using the exact wire format desktop publishes: kind 30078, d-tag channel-sort, NIP-44 encrypted-to-self, whole-blob LWW, same group keys (starred/channels/dms/section:<id>, alpha/recent). Includes the same dirty-flag protection.
  • Per-group Sort: Recent / A–Z menu on Starred, custom sections, Channels, and DMs; sort semantics match desktop's sortChannelsForSidebar (recent = newest first, message-less channels sink alphabetically). Orphaned section:<id> keys pruned on write.
  • Offline local caching and encrypted relay semantics preserved throughout.

Verification

  • 23 focused tests: teardown/rebuild clobber regression, publish-on-reconnect, dirty-flag persistence + clearing, seed-publish gating (incl. no-seed-on-failed-fetch), live-event-while-dirty, desktop wire-format round-trip, LWW adoption, orphan pruning, sort comparators.
  • Full mobile suite: 588 passed, flutter analyze clean, dart format clean.
  • Cross-impl NIP-44 decrypt of desktop (rust-nostr) ciphertext verified during investigation.

…el sort

Mobile channel-section edits were silently lost: sections publish on a 5s
debounce, but the provider tears the sync manager down on every relay-session
status flip without flushing. The rebuilt manager then fetched the stale
remote blob and unconditionally adopted it over newer local state, erasing
the just-made edit ("groups do not persist").

Fix, mirroring the read-state manager pattern:
- Persisted dirty flag (dirty-since) in ChannelSectionsStorage so
  unpublished edits survive manager teardown and app restarts.
- Dirty-state protection in _mergeEvent: a remote blob never overwrites
  unpublished local edits; the pending publish reconciles the relay.
- Publish-on-reconnect: initialize() re-schedules a publish when the dirty
  flag is set, and seed-publishes local state when the relay confirms it has
  no blob (never on a failed fetch).
- Edit-generation guard so a publish only clears the dirty flag when no new
  edit landed while it was in flight.

Also adds mobile channel-sort sync parity (desktop-only until now):
- New channel_sort feature (storage/manager/provider) using the same
  encrypted NIP-78 blob desktop publishes (kind 30078, d-tag channel-sort,
  NIP-44 to self, whole-blob LWW) including the dirty-flag protection above.
- Per-group Sort: Recent / A-Z controls on Starred, custom sections,
  Channels, and DMs, matching desktop group keys (starred/channels/dms,
  section:<id>) and sort semantics; orphaned section:<id> keys are pruned
  on write.

Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
@tellaho
tellaho requested a review from a team as a code owner July 25, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant