Summary
Custom sidebar sections do not sync between the mobile app and the desktop app in either direction. Both platforms ship the feature and both claim to sync it over the same NIP-78 blob (#792 desktop, #800 mobile), but in practice each device keeps its own organization:
- Move a channel into a section on the phone → on desktop that channel is still unorganized (sits in the flat Channels group).
- Organize channels into sections on desktop → the phone does not reflect it.
The sections themselves render fine locally on each platform. It's the cross-device state that never converges.
Reproduction
- On desktop, create a section (e.g.
Ops) and drag a channel into it.
- Open the same community on mobile with the same identity, fully connected to the relay.
- Phone shows the channel in the default group, not in
Ops.
- On the phone, long-press a channel → Move to section → pick/create a section.
- Return to desktop. The channel is not in that section.
Both apps are online and connected to the relay throughout; other synced state (messages, read state) is flowing normally.
What I checked in the code
The wire contract looks aligned on both sides, which is why this reads as a real bug rather than an unimplemented feature:
- Same event: kind
30078, ["d","channel-sections"], ["t","channel-sections"]
(desktop/src/features/sidebar/lib/channelSectionsSync.ts, mobile/lib/features/channels/channel_sections/channel_sections_manager.dart)
- Same payload shape:
{version, sections:[{id,name,icon,order}], assignments:{channelId → sectionId}}
- Same crypto: NIP-44 encrypt-to-self
- Same merge policy: whole-blob last-write-wins, newer
createdAt wins, event-id tie-break
Two things stood out as candidate causes:
1. The synced blob is not scoped per community, but the desktop cache is.
#1477 (fix(sidebar): scope channel sections storage to relay URL) scoped desktop's local store by normalized relay URL:
// desktop/src/features/sidebar/lib/channelSectionsStorage.ts
export function storageKey(pubkey: string, relayUrl?: string): string {
if (!relayUrl) return `${STORAGE_KEY_PREFIX}:${pubkey}`;
const normalized = normalizeRelayUrl(relayUrl);
...
Mobile's key was never given the same treatment:
// mobile/lib/features/channels/channel_sections/channel_sections_storage.dart:5
String channelSectionsKey(String pubkey) => 'buzz.channel-sections.v1:$pubkey';
Meanwhile the relay event carries no community/relay discriminator at all — the d tag is the constant "channel-sections" on both platforms. So one identity has exactly one sections blob across every community it's a member of, while desktop maintains a separate local view per relay. For a multi-community user that means each publish overwrites the other community's organization wholesale, and the two platforms disagree about what the blob is even supposed to contain.
2. Every failure on this path is silent.
Decrypt/parse failures are swallowed on both sides (catch {} in decryptAndParse, catch (_) {} in _mergeEvent), and desktop's publish failure only reaches console.warn. If any of the above is misfiring at runtime there is no user-visible signal and nothing in the UI to distinguish "not synced yet" from "sync is broken."
Suggestion
- Scope the synced blob per community — e.g.
["d", "channel-sections:<community>"] (with a migration read of the legacy unscoped d tag), and scope mobile's local key the same way desktop's is.
- Surface sync failures instead of swallowing them — at minimum a debug-visible signal when a fetched blob fails to decrypt or parse, since today both platforms fail closed and look identical to "no remote state."
- A cross-platform test that publishes a blob from one client's serializer and reads it back through the other's parser would catch schema/scope drift before it reaches users.
Related
Summary
Custom sidebar sections do not sync between the mobile app and the desktop app in either direction. Both platforms ship the feature and both claim to sync it over the same NIP-78 blob (#792 desktop, #800 mobile), but in practice each device keeps its own organization:
The sections themselves render fine locally on each platform. It's the cross-device state that never converges.
Reproduction
Ops) and drag a channel into it.Ops.Both apps are online and connected to the relay throughout; other synced state (messages, read state) is flowing normally.
What I checked in the code
The wire contract looks aligned on both sides, which is why this reads as a real bug rather than an unimplemented feature:
30078,["d","channel-sections"],["t","channel-sections"](
desktop/src/features/sidebar/lib/channelSectionsSync.ts,mobile/lib/features/channels/channel_sections/channel_sections_manager.dart){version, sections:[{id,name,icon,order}], assignments:{channelId → sectionId}}createdAtwins, event-id tie-breakTwo things stood out as candidate causes:
1. The synced blob is not scoped per community, but the desktop cache is.
#1477(fix(sidebar): scope channel sections storage to relay URL) scoped desktop's local store by normalized relay URL:Mobile's key was never given the same treatment:
Meanwhile the relay event carries no community/relay discriminator at all — the
dtag is the constant"channel-sections"on both platforms. So one identity has exactly one sections blob across every community it's a member of, while desktop maintains a separate local view per relay. For a multi-community user that means each publish overwrites the other community's organization wholesale, and the two platforms disagree about what the blob is even supposed to contain.2. Every failure on this path is silent.
Decrypt/parse failures are swallowed on both sides (
catch {}indecryptAndParse,catch (_) {}in_mergeEvent), and desktop's publish failure only reachesconsole.warn. If any of the above is misfiring at runtime there is no user-visible signal and nothing in the UI to distinguish "not synced yet" from "sync is broken."Suggestion
["d", "channel-sections:<community>"](with a migration read of the legacy unscopeddtag), and scope mobile's local key the same way desktop's is.Related