Skip to content

feat(mobile): add channel sections with relay sync#800

Merged
wpfleger96 merged 8 commits into
mainfrom
wpfleger/channel-sections-mobile
Jun 1, 2026
Merged

feat(mobile): add channel sections with relay sync#800
wpfleger96 merged 8 commits into
mainfrom
wpfleger/channel-sections-mobile

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Adds channel sections to the mobile app, bringing it to full parity with the desktop feature introduced in #789 and #792.

Users who organize their channels into sections on desktop currently see a flat list on mobile. This PR renders those sections, supports creating/renaming/deleting/reordering them, lets users assign channels via long-press, and syncs bidirectionally with desktop through the same NIP-78 kind 30078 events and NIP-44 encryption established in #792. Long-pressing any channel opens an action sheet with "Move to section" and a mark read/unread toggle (matching the desktop right-click menu).

  • channel_sections_storage.dart — SharedPreferences persistence with key sprout.channel-sections.v1:<pubkey>, matching the desktop format exactly so the same relay events decode correctly on both platforms
  • channel_sections_manager.dartChannelSectionsCrypto (NIP-44 encrypt-to-self) + ChannelSectionsManager with full CRUD, 5-second debounced publish, kind 30078 fetch/subscribe, and last-write-wins merge (newer createdAt wins; event ID tie-break)
  • channel_sections_provider.dartChannelSectionsNotifier Riverpod provider watching relayConfigProvider, relaySessionProvider, and activeWorkspaceProvider; follows ReadStateNotifier pattern throughout
  • channels_page.dart — renders user-defined _CustomChannelSection widgets above the built-in "Channels" group; adds _CustomSectionHeader with PopupMenuButton (rename/move up/move down/delete); adds long-press on _ChannelTile opening an action sheet with "Move to section" and an always-present mark read/unread toggle; _SectionNameDialog for create/rename flows
  • read_state_manager.dart / read_state_provider.dart — adds markContextUnread (rolls read timestamp back to lastMessage - 1) to support the mark-as-unread toggle, mirroring the desktop ReadStateManager pattern

Stack: #789 (desktop UI) → #792 (relay sync) → this PR (mobile)

@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-sync branch from 02d06b9 to 2b5e69e Compare May 29, 2026 22:32
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from c353ec6 to acd0e36 Compare May 29, 2026 22:33
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

channel sections on mobile
image

@wpfleger96

wpfleger96 commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Channel section changes sync across devices in realtime via Nostr

sprout_channel_sections_sync_to_mobile

@wpfleger96 wpfleger96 marked this pull request as ready for review May 29, 2026 22:47
@wpfleger96 wpfleger96 requested a review from a team as a code owner May 29, 2026 22:47
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-sync branch from 2b5e69e to 166c741 Compare June 1, 2026 16:21
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from acd0e36 to 1772344 Compare June 1, 2026 16:22
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-sync branch from 166c741 to e604da9 Compare June 1, 2026 16:48
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from 1772344 to b4139dc Compare June 1, 2026 16:49
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-sync branch from e604da9 to 7ed25fe Compare June 1, 2026 17:23
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from b4139dc to 72301b5 Compare June 1, 2026 17:23
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

Adding/removing sections on mobile:

sprout_add+remove_sections_mobile

@wesbillman

Copy link
Copy Markdown
Collaborator

Review note (Brain) — mobile parity, part of the channel-sections stack

Reviewed as the tip of the 3-PR stack (#789#792 → this). The mobile implementation itself looks good: it faithfully mirrors the desktop manager (CRUD, 5s debounced publish, LWW merge, markContextUnread) and uses the same kind 30078 + "channel-sections" d-tag + NIP-44 encrypt-to-self contract, so events round-trip with desktop.

No changes needed in this PR — but it's blocked on a fix in #792. #792's desktop LWW tie-break is currently inverted relative to this PR's mobile tie-break (desktop: smaller event id wins on a same-second tie; mobile here: larger id wins — channel_sections_manager.dart:265-268). If #792 merges as-is, the two platforms can converge to different section layouts. The agreed fix is on the desktop side (#792); this mobile code is the correct reference behavior ("larger id wins"), so leave it as-is.

One small confirmation from the review: the mobile debounce-vs-remote race is safe — the delayed _publish() serializes the current _store at fire time (not a captured stale store), and _mergeEvent updates _store before any publish could read it, so an inbound remote isn't clobbered by a pending local debounce.

Recommendation: merge after #789 and the corrected #792.

— Brain (paired with @Pinky on this review)

@wesbillman

Copy link
Copy Markdown
Collaborator

@wpfleger96 my agents added some comments for ya.

Also, the spacing here looks like it's too much in the group sections. Can we check that out?
Screenshot 2026-06-01 at 1 38 28 PM

@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from 72301b5 to 4a488e4 Compare June 1, 2026 19:40
Channel sections were localStorage-only, meaning they didn't sync
across devices. Adds a NIP-78 (kind 30078) event-based sync layer
so sections created, renamed, deleted, or reordered on any device
propagate to all others. localStorage is retained as a local cache
for instant rendering on startup.
Cancel stale debounce when applying remote updates to prevent
publishing overwritten state. Track pending publishes for retry on
reconnect. Add pubkey validation before decrypting relay events.
Extract applyRemote reconcile helper and swapSectionOrder to
eliminate duplication. Fix createSection to construct section before
setStore so return value is deterministic. Add NIP-01 event ID
tie-breaking. Wire resetSyncState into resetWorkspaceState.
Extract swapSectionOrder to channelSectionsHelpers.ts for testability.
27 tests covering parseChannelSectionPayload validation,
stripOrphanedAssignments identity semantics, localStorage round-trip,
and swapSectionOrder boundary conditions.
…eorder

Two review findings in useChannelSections.ts:

1. The same-second tie-break was inverted vs mobile — desktop kept the
   smaller event ID while mobile kept the larger. Flip >= to <= so both
   platforms converge to the lexicographically larger event ID.

2. reorderSections (the DnD path) wrote to localStorage but never called
   publishSections, so drag-reordered sections didn't sync to other
   devices until some unrelated mutation triggered a publish.
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-sync branch from 133a3cf to a4b6ed5 Compare June 1, 2026 19:44
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from 4a488e4 to b16cd64 Compare June 1, 2026 19:45
…y assertion

Add version: 1 to the published payload to match mobile's format,
preventing future migration asymmetry. Replace pubkey! non-null
assertion with an early return guard to silence the Biome warning.
User-defined channel groups for the mobile sidebar, matching the desktop
feature. Sections are persisted locally via SharedPreferences and synced
across devices through NIP-78 kind 30078 events with NIP-44 encryption,
using the same data format as desktop so changes propagate bidirectionally.

Adds CRUD (create/rename/delete sections, move up/down), channel
assignment via long-press action sheet, and section rendering above the
ungrouped "Channels" group. Follows the ReadStateManager pattern for
relay fetch/subscribe/publish with 5-second debounced writes and
last-write-wins conflict resolution.
The bottom sheet only showed "Mark as read" when a channel was unread.
Now it always shows a toggling item matching desktop parity. Adds
markContextUnread to ReadStateManager (rolls back to lastMessage - 1).
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from b16cd64 to 4c7babd Compare June 1, 2026 19:51
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

🤖 good eye on the spacing — the PopupMenuButton's default 48px minimum tap target was inflating the custom section header beyond the built-in header height. fixed in 4c7babd by wrapping it in a SizedBox(24x24) with empty BoxConstraints to match the natural row height.

Base automatically changed from wpfleger/channel-sections-sync to main June 1, 2026 20:02
PopupMenuButton inflates its tap target beyond the Row's natural height,
creating a visible gap between custom section titles and channels. Replace
it with GestureDetector + showMenu() so the Row contains only plain Icon
and Text widgets — structurally identical to the built-in _SectionHeader.
@wpfleger96 wpfleger96 force-pushed the wpfleger/channel-sections-mobile branch from 4c7babd to 5f58577 Compare June 1, 2026 20:23
@wpfleger96 wpfleger96 enabled auto-merge (squash) June 1, 2026 20:23
@wpfleger96 wpfleger96 merged commit 5cbedb1 into main Jun 1, 2026
15 checks passed
@wpfleger96 wpfleger96 deleted the wpfleger/channel-sections-mobile branch June 1, 2026 20:32
tlongwell-block pushed a commit that referenced this pull request Jun 1, 2026
* origin/main:
  chore(release): release version 0.3.6 (#811)
  feat(mobile): add channel sections with relay sync (#800)
  feat(desktop): sync channel sections across devices via Nostr (#792)
  feat(media): support arbitrary file types with download cards (#810)
  feat(desktop): add user-defined channel sections to sidebar (#789)

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>

# Conflicts:
#	desktop/src-tauri/src/commands/mod.rs
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.

2 participants