Draw the sidebar with folders, and drag channels into them - #401
Merged
Conversation
A folder is a row you click to open and shut. A channel goes in by being dragged right past a threshold, and all the way left brings it back out. Straight up and down keeps whatever folder it was already in, so reordering inside a folder does not throw things out of it. Reorder used to send a bare list of ids. One drag now changes the order and the folder together, so it sends both, with every entry naming its folder outright. The flatten-and-drop logic is its own module because it is the part with cases in it. An orphan whose folder is gone goes to the top level rather than disappearing, and a child of a collapsed folder is put back into the payload after its folder, so collapsing one and dragging something else does not empty it. A shut folder carries what its children cannot say for themselves, which is the selected state when the open channel is inside it plus the unread dot and mention count rolled up. renderChannel already carries the note about "you are here" being the one thing the list stopped saying, and closing a folder around the open channel is that failure by another route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 5, 2026
* Set how loud a server, a folder or a channel is Three scopes, most specific first: a channel's own setting beats its folder's, which beats the server's, which falls back to hearing everything. Muting a server therefore quietens it without overruling a channel somebody has already had an opinion about. Kept on the device and never sent anywhere. A list of the channels somebody has muted is a list of what they are avoiding, and the server has no use for it: every notification this gates is produced by the client, from events the server was going to send regardless. The cost is that it does not follow you to another machine, which is worth saying plainly. "Only mentions" is a real level rather than a quieter way of saying none. chat:new carries no mention flag, so the sound for that level comes off mention:new, which the client already receives to keep its counts. Two listeners on one event, answering different questions. Unread is marked whatever the level says. Muting a channel is about not being interrupted, not about pretending nothing happened there. The menu is no longer manage-only. How loud a channel is belongs to the person hearing it, so somebody who cannot rearrange the sidebar still gets the notification choice and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Sort the notificationPrefs export simple-import-sort wanted it above singletonHook, and CI runs eslint with --max-warnings 0, so the branch was failing lint on this one line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of three, on top of Gryt-chat/server#137. The mobile half follows.
The gesture
axis="y"pins the row to the column, but the pointer isn't pinned, andinfo.offset.xstill reports where it went. So the row stays in line while thecursor decides the depth.
That last one is what makes dragging a channel up and down inside its own folder
work. Without it, every reorder within a folder would eject the thing being
reordered.
The row draws itself indented the moment it crosses the threshold, before the
drop, so the folder it's about to join is visible rather than guessed at.
Folders as accordions
Clicking a folder opens and shuts it and does nothing else, so it never steals
the selection from the channel you're reading. Collapse state is per server, per
device, in localStorage. Two people looking at the same sidebar can have
different folders open, and an operator collapsing one shouldn't fold it up for
everybody.
What a shut folder says
You asked whether the folder should light up when the channel you're in is
inside it. Yes, and the reason is already in this file.
renderChannelcarriesthis:
Collapsing a folder removes that row from the DOM, so the same failure comes
back another way. The folder carries it instead: accent fill when it holds the
open channel, and the unread dot and mention count of its hidden children rolled
onto it. Without the rollup, collapsing a folder would quietly mute everything
in it, which somebody would do by accident and then not be able to explain.
All of that applies only while shut. Open, each child says it for itself, and a
lit folder above already-lit rows says the same thing twice.
sidebarTree.ts
Flatten, drop resolution and payload building are a module rather than more of
ChannelList, because they're the part with cases in them and the component isalready 468 lines.
The failures worth pinning are the ones that drop a row rather than misplace it,
since a channel is only on screen if a row is drawn for it:
order, and it has to be put back into the payload after its folder or the
server would renumber it out of the folder
What to look at
resolveDropParent, and the rule that a top-level non-folder row ends the run.Without it, a channel dropped below an unrelated channel would silently join a
folder further up the list.
What I checked
tsc -bandeslintclean.check-sidebar-tree.mjshas 18 assertions across 13cases, wired into CI, covering the flatten, all three drag outcomes, and both
payload traps.
The whole client CI set passes apart from
test:updater-bridgeandtest:embedded-version, the two already broken on main and confirmed failing ona clean checkout without this branch.
I did get this running against a live server in the end. Creating a folder from
the right-click menu works, and it lands in SQLite as a
folderrow. Childrenrender indented 14px under their parent. Collapsing hides them and persists to
gryt_sidebar_collapsed:<host>. A collapsed folder holding the open channeltakes
bg-gryt-accentand paints#968ff8on#0c0a20.Two things I couldn't check there. The drag gesture doesn't respond to synthetic
pointer events, so
resolveDropParentis still only covered by its unit tests.And the preview pane freezes
requestAnimationFramewhile it's hidden, so exitanimations never finish and rows stay on screen after a collapse. That's the
harness rather than the code, and it sent me chasing a bug that wasn't there.
🤖 Generated with Claude Code