Skip to content

feat(channels): inline sidebar drag-reorder & text-type switching#107

Merged
krazyjakee merged 2 commits into
masterfrom
feat/inline-channel-drag-and-type-switch
Jun 10, 2026
Merged

feat(channels): inline sidebar drag-reorder & text-type switching#107
krazyjakee merged 2 commits into
masterfrom
feat/inline-channel-drag-and-type-switch

Conversation

@krazyjakee

Copy link
Copy Markdown
Contributor

Summary

  • Inline channel drag-reorder: managers (manage-channels permission) get a ReorderableListView sidebar — long-press to drag channels and categories into a new order, or move a channel under a different category. Dragging a category carries its children as a contiguous block. Changes persist via per-bucket position (plus parent_id when a channel crosses a category boundary) with optimistic PATCH and end-of-run reconciliation. Non-managers keep the plain ListView.
  • Channel type switching: the channel edit dialog gains a Type selector limited to non-destructive textannouncement conversions (mirrors accordserver's is_non_destructive_type_change). Voice/category/forum show no selector since no safe conversion exists.

This is an inline alternative to PR #100's modal reorder dialog — reordering happens directly in the sidebar.

Test plan

  • As a manager, long-press-drag a channel within a category and confirm order persists after refresh
  • Drag a channel from one category to another; confirm parent_id updates
  • Drag a whole category (with children) to a new position
  • Collapsed categories keep their children correctly parented
  • As a non-manager, sidebar is the plain non-draggable list
  • Edit a text channel → switch type to announcement (and back); confirm no data loss
  • Voice/category/forum edit dialogs show no Type selector

🤖 Generated with Claude Code

krazyjakee and others added 2 commits June 10, 2026 23:33
Add a ReorderableListView channel sidebar for managers (long-press to
drag channels/categories, move channels between categories, persisted
via per-bucket position + parent_id PATCH), and a Type selector in the
channel edit dialog for non-destructive text<->announcement conversions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… add tests

- _persist(): add _pendingPersist + _schedulePersist() so rapid drags never
  run two concurrent async persist operations; the second persist restarts
  after the first finishes rather than racing it.

- _recomputeParents(): take the moved _DragEntry as a parameter and only
  update that entry's parentId. The old all-items walk would silently
  reparent uncategorized channels when a category was dragged before them,
  sending destructive parent_id PATCHes to the API.

- Extract parseChannelPosition / channelListSignature to
  lib/features/channels/utils/channel_sort.dart; _pos in both
  accord_home_channels.dart and accord_channel_reorder.dart now delegate
  there, removing the three-way duplication with accord_channels.dart's
  _asInt.

- Add test/features/channels/channel_sort_test.dart covering int/num/
  String/null position parsing and signature order-independence.

- Trim multi-line doc comments on private classes to single lines per
  project convention (CLAUDE.md: "one short line max").

https://claude.ai/code/session_018BoRo7xUkKYJ9ffh8tQfes

Copy link
Copy Markdown
Contributor Author

Review findings & fixes — commit fbae138

All findings below have been fixed and pushed to this branch.


🔴 Critical — Bug

1. Race condition in _persist() (concurrent async persists)

_onReorder called _persist() unconditionally, even when a previous persist was still awaiting network responses. Two concurrent persists both held a reference to _items, both would call _resync() independently, and the _persisting guard in didUpdateWidget could be cleared by the first finishing persist while the second was still in-flight — allowing a stale gateway snapshot to overwrite the optimistic local state.

Fix: Added _pendingPersist flag and _schedulePersist(). If a persist is already running, the new drag is queued; _persist()'s finally block restarts one more persist with the latest _items instead of calling _resync().


2. _recomputeParents() silently reparents uncategorized channels on category drag

The inline drag list places uncategorized channels before all categories in _items. When a user dragged a category to position 0 (before the uncategorized section), the old _recomputeParents() walked the entire list and assigned every uncategorized channel's parentId to that category. _persist() then sent destructive parent_id PATCHes to the API, moving channels the user never touched.

Fix: _recomputeParents now takes the dragged _DragEntry as a parameter and only updates that entry's parentId based on the nearest preceding category. Category drags return early — the category and its children keep their original parentIds.


🟡 Medium — Code quality / maintainability

3. _pos logic duplicated three times

The position-parsing helper (handle int / num / String / null → 0) existed identically in _ChannelDragListState._pos, _ChannelReorderState._pos, and accord_channels.dart's _asInt.

Fix: Extracted to lib/features/channels/utils/channel_sort.dart as parseChannelPosition (and channelListSignature for the signature helper). Both view files now delegate to the shared function.


🟢 Low — Test coverage

4. No tests for the position-parsing / signature logic

The _pos type-coercion and _signatureOf change-detection code had no test coverage. These are the functions that determine whether a reorder triggers a backend PATCH.

Fix: Added test/features/channels/channel_sort_test.dart covering int / double / string / null / non-numeric position parsing and signature order-independence / field-sensitivity.


🟢 Low — Style

5. Multi-line doc comments on private classes

Three new private classes (_ChannelDragList, _DragEntry, _DragUpdate) had multi-line /// blocks explaining what the code does. Per project convention (CLAUDE.md: "one short line max", "Don't explain WHAT the code does"), these were trimmed to single-line summaries. The inline comment describing the ternary in _ChannelListState.build() was also removed for the same reason.


ℹ️ Not fixed — noted for awareness

  • _buildChannelEntries (non-manager path) doesn't sort channels by position, while _flatten (manager path) does. This inconsistency pre-dates this PR; the channel controller's _sorted() provides channels in position order so both paths should render correctly in practice.
  • The "Reorder channels" modal button in the header is still present alongside the new inline drag. Keeping both is arguably good for accessibility / keyboard users; removing the button would be a separate UX decision.
  • onReorderItem semantics — the comment claims it passes a post-removal newIndex (different from the standard onReorder callback, which requires if (newIndex > oldIndex) newIndex--). This matches the code's usage and is consistent with Flutter's intent for onReorderItem. No change made, but worth a manual drag test to confirm correct end-positions.

Generated by Claude Code

@krazyjakee krazyjakee merged commit cfd7b54 into master Jun 10, 2026
5 checks passed
@krazyjakee krazyjakee deleted the feat/inline-channel-drag-and-type-switch branch June 10, 2026 22:52
krazyjakee added a commit that referenced this pull request Jun 14, 2026
…oo (#142)

* fix(channels): use ReorderableListView.onReorder; gate CI on stable too

`accord_home_channels.dart` called `ReorderableListView.builder` with
`onReorderItem:`, which isn't a parameter on Flutter's built-in widget — it
requires `onReorder:`. This was a hard `flutter analyze` error on stable
(missing_required_argument + undefined_named_parameter), breaking any build
touching the file. Fixes #138.

The two APIs differ in `newIndex` semantics: `onReorderItem` reported the
target index in post-removal coordinates, while `onReorder` reports it
pre-removal (the dragged slot is still counted). Renamed the argument and
added the canonical `if (newIndex > oldIndex) newIndex -= 1;` so the existing
reorder logic — which indexes into the post-removal list — stays correct and
drag-down moves don't land one slot too low.

Why it reached master: the error landed in #107 and CI was green, because the
analyze gate only ran the pinned `beta` channel, which tolerated
`onReorderItem`. Matrix the analyze/test job over `[beta, stable]` so a
widget/SDK API that diverges between channels fails the gate instead of
shipping. Cache keys now include the channel so the legs don't collide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review(#142): add reorder-index tests and clarify FLUTTER_CHANNEL comment

- Add 7 unit tests in channel_sort_test.dart covering the canonical
  `if (newIndex > oldIndex) newIndex -= 1` adjustment that fixes #138:
  drag-down by one slot (no-op), drag-down multiple slots, drag-down to
  last position, drag-up variants, and a regression guard that documents
  the exact wrong result produced without the fix.
- Clarify the FLUTTER_CHANNEL env var comment in ci.yml: the analyze job
  now uses matrix.channel (beta + stable), so the env var only controls
  the build job — the previous comment implied it still drove analyze.

https://claude.ai/code/session_0171kkokchuut5V7nmtibzLc

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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