Skip to content

[Bug] Custom sidebar sections do not sync between mobile and desktop (either direction) #4883

Description

@jdrolls

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

  1. On desktop, create a section (e.g. Ops) and drag a channel into it.
  2. Open the same community on mobile with the same identity, fully connected to the relay.
  3. Phone shows the channel in the default group, not in Ops.
  4. On the phone, long-press a channel → Move to section → pick/create a section.
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions