Skip to content

fix(mobile): scope channel sections storage to relay URL - #5761

Open
crgallego wants to merge 3 commits into
block:mainfrom
crgallego:fix/mobile-channel-sections-relay-scope
Open

fix(mobile): scope channel sections storage to relay URL#5761
crgallego wants to merge 3 commits into
block:mainfrom
crgallego:fix/mobile-channel-sections-relay-scope

Conversation

@crgallego

@crgallego crgallego commented Aug 13, 2026

Copy link
Copy Markdown

Fixes #5762

Summary

Mobile custom channel folders (sections) were stored account-global (buzz.channel-sections.v1:$pubkey), so switching linked workspaces showed empty folders from other communities (e.g. Coker Construction folders on Lit Box with no channels inside).

Desktop already fixed this in #1477 by scoping local storage to pubkey + relay URL. Mobile channel-sort already uses the same pattern. This PR ports that scoping to mobile channel sections.

Changes

  • Scope SharedPreferences keys by normalized relay origin (pubkey + encodeURIComponent(relayUrl))
  • One-time migration: first active relay after upgrade claims the legacy unscoped blob, then the legacy key is deleted so later communities stay clean
  • Provider remounts on active community change and passes relayUrl into the manager
  • Storage tests for isolation, migration, corrupt payloads, and empty-legacy no-op

Notes

  • Remote NIP-78 channel-sections blobs remain relay-local by nature of publish/fetch on the connected relay; the bug was local prefs bleeding across communities when remote was empty/absent
  • Migration tradeoff matches Desktop fix(sidebar): scope channel sections storage to relay URL #1477 / mobile channel-sort: first opened workspace after upgrade inherits the old global layout

Test plan

  • dart analyze lib/features/channels/channel_sections/ test/features/channels/channel_sections/ — clean
  • flutter test test/features/channels/channel_sections/15/15 passed
  • Manual: create section folders on community A, assign channels, switch to community B → A folders must not appear empty on B
  • Manual: after upgrade, open original community first → folders preserved; second community stays empty until you create sections there

Port Desktop block#1477 to mobile so custom channel folders (sections) no
longer bleed across linked workspaces/communities. Local prefs are now
keyed by pubkey + normalized relay origin, with a one-time migration
from the legacy account-global key onto the first active relay only.

Co-authored-by: crgallego <chris.cokerconstruction@gmail.com>
Signed-off-by: crgallego <chris.cokerconstruction@gmail.com>
@crgallego

Copy link
Copy Markdown
Author

Tracking issue: #5762

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the relayUrl fallback diverges from the pattern this ports, and it interacts badly with the one-time migration.

channel_sort_provider.dart:56-59 uses activeCommunity?.relayUrl only and returns empty state when it's absent; here the else-branch falls back to relayConfig.baseUrl. activeCommunityProvider is a FutureProvider (community_provider.dart:103), so .value is null on the first build of every cold start — the first mount reads and writes under a baseUrl-derived key rather than the community one.

that's also the mount that consumes the migration: read() finds the scoped key empty, copies the legacy blob into the baseUrl-scoped key and deletes legacyChannelSectionsKey. when the future resolves and the provider rebuilds under activeCommunity.relayUrl, that key is empty and the legacy key is gone — so on any device where baseUrl doesn't normalize to exactly the community's relayUrl, the upgrade loses the sections instead of migrating them.

returning const ChannelSectionsState() while activeCommunity is null, the way channel-sort does, avoids both. the normalize helper itself is a faithful copy of normalizeChannelSortRelayUrl.

Match the channel-sort pattern this ports: when the active community has
not resolved, return an empty state instead of falling back to
relayConfig.baseUrl. The fallback could only ever key storage differently
from the community-scoped key, and the manager consumes the one-time
legacy migration in its constructor, so a divergent first read would
delete the legacy blob under the wrong key.

Co-authored-by: crgallego <chris.cokerconstruction@gmail.com>
Signed-off-by: crgallego <chris.cokerconstruction@gmail.com>
activeCommunityProvider is a FutureProvider, so the first mount has a
null community. The old baseUrl fallback would consume the one-time
legacy migration under the wrong key. This test keeps nsec present
while the community is still loading and asserts the blob stays put
until the community relay URL is known.

Co-authored-by: crgallego <chris.cokerconstruction@gmail.com>
Signed-off-by: crgallego <chris.cokerconstruction@gmail.com>
@crgallego

Copy link
Copy Markdown
Author

Agreed — the relayConfig.baseUrl fallback was the wrong pattern, and it could consume the one-time migration under the wrong key.

Two follow-up commits are on the branch:

  • a62f67d4 — match channel-sort: if activeCommunity?.relayUrl is missing, return const ChannelSectionsState() and do not construct the manager (so read() never runs, so the legacy key is not claimed).
  • e9d85a49 — provider regression: nsec is present, community future is still pending, baseUrl and community origin differ. First mount must leave the legacy blob untouched; after the community resolves, migration lands only on the community-scoped key.

One extra reason the fallback was unsafe even for “the same relay”: RelayConfig.baseUrl folds wss://https://, so it would not normalize to the stored community origin.

Locally at the test commit: flutter analyze clean on the sections package, flutter test 1298/1298.

@crgallego

Copy link
Copy Markdown
Author

Posting as Claude, an AI agent running inside @crgallego's Buzz workspace. He asked me to reply on his behalf, and to say so plainly rather than let this read as a human review response.

Thanks for catching this — the change is in, and it is the right change. One correction to the mechanism, though, because the PR should not carry a wrong story about why.

The fallback branch was unreachable in production

build() returns early when relayConfig.nsec is empty (channel_sections_provider.dart:37-40), and nsec comes from the same activeCommunityProvider (relay_provider.dart:80-87) — when the community is null, RelayConfigNotifier.build() falls through to const RelayConfig(baseUrl: Env.relayUrl), which carries no nsec. So on a real cold start the provider bails before constructing a manager; it never reached line 56.

The regression test in e9d85a49 has to override relayConfigProvider with a fake holding an nsec while the community future is still pending — a state the real notifier cannot produce. That is still a worthwhile guard against reintroducing the fallback. It just is not reproducing a shipped data-loss path.

One thing you understated

ChannelSectionsManager reads storage in its constructor (channel_sections_manager.dart:79), not in the deferred initialize(). There was no race window to lose: had the branch ever been reachable, the first construction would have consumed the legacy blob synchronously, inside build().

And your scheme concern is stronger than you put it

You wrote "any device where baseUrl doesn't normalize to exactly the community's relayUrl." It is every invite-joined community, by construction. Communities persist relayUrl raw — invite join stores wss://, device pairing stores https:// — while RelayConfig.baseUrl folds wss://https:// (relay_provider.dart:43-51), and normalizeChannelSectionsRelayUrl only lowercases and strips trailing slashes. It never folds schemes.

Net

Latent hazard rather than a live bug, and removing it is unambiguously correct — the ported pattern was right and this diverged from it for no benefit.

Verified at e9d85a49 with a clean working tree: flutter analyze clean, dart format clean, flutter test 1298/1298.

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.

fix(mobile): channel section folders bleed across linked workspaces

3 participants