Add channel reordering functionality
Release Notes:
- Added channel reordering for administrators (use
cmd-upandcmd-downon macOS orctrl-upctrl-downon Linux to move channels up or down within their parent)
Summary
This PR introduces the ability for channel administrators to reorder channels within their parent context, providing better organizational control over channel hierarchies. Users can now move channels up or down relative to their siblings using keyboard shortcuts.
Problem
Previously, channels were displayed in alphabetical order with no way to customize their arrangement. This made it difficult for teams to organize channels in a logical order that reflected their workflow or importance, forcing users to prefix channel names with numbers or special characters as a workaround.
Solution
The implementation adds a persistent channel_order field to channels that determines their display order within their parent. Channels with the same parent are sorted by this field rather than alphabetically.
Implementation Details
Database Schema
Added a new column and index to support efficient ordering:
-- crates/collab/migrations/20250530175450_add_channel_order.sql
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;
CREATE INDEX CONCURRENTLY "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
RPC Protocol
Extended the channel proto with ordering support:
// crates/proto/proto/channel.proto
message Channel {
uint64 id = 1;
string name = 2;
ChannelVisibility visibility = 3;
int32 channel_order = 4;
repeated uint64 parent_path = 5;
}
message ReorderChannel {
uint64 channel_id = 1;
enum Direction {
Up = 0;
Down = 1;
}
Direction direction = 2;
}
Server-side Logic
The reordering is handled by swapping channel_order values between adjacent channels:
// crates/collab/src/db/queries/channels.rs
pub async fn reorder_channel(
&self,
channel_id: ChannelId,
direction: proto::reorder_channel::Direction,
user_id: UserId,
) -> Result<Vec<Channel>> {
// Find the sibling channel to swap with
let sibling_channel = match direction {
proto::reorder_channel::Direction::Up => {
// Find channel with highest order less than current
channel::Entity::find()
.filter(
channel::Column::ParentPath
.eq(&channel.parent_path)
.and(channel::Column::ChannelOrder.lt(channel.channel_order)),
)
.order_by_desc(channel::Column::ChannelOrder)
.one(&*tx)
.await?
}
// Similar logic for Down...
};
// Swap the channel_order values
let temp_order = channel.channel_order;
channel.channel_order = sibling_channel.channel_order;
sibling_channel.channel_order = temp_order;
}
Client-side Sorting
Optimized the sorting algorithm to avoid O(n²) complexity:
// crates/collab/src/db/queries/channels.rs
// Pre-compute sort keys for efficient O(n log n) sorting
let mut channels_with_keys: Vec<(Vec<i32>, Channel)> = channels
.into_iter()
.map(|channel| {
let mut sort_key = Vec::with_capacity(channel.parent_path.len() + 1);
// Build sort key from parent path orders
for parent_id in &channel.parent_path {
sort_key.push(channel_order_map.get(parent_id).copied().unwrap_or(i32::MAX));
}
sort_key.push(channel.channel_order);
(sort_key, channel)
})
.collect();
channels_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
User Interface
Added keyboard shortcuts and proper context handling:
// assets/keymaps/default-macos.json
{
"context": "CollabPanel && not_editing",
"bindings": {
"cmd-up": "collab_panel::MoveChannelUp",
"cmd-down": "collab_panel::MoveChannelDown"
}
}
The CollabPanel now properly sets context to distinguish between editing and navigation modes:
// crates/collab_ui/src/collab_panel.rs
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("CollabPanel");
dispatch_context.add("menu");
let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window) {
"editing"
} else {
"not_editing"
};
dispatch_context.add(identifier);
dispatch_context
}
Testing
Comprehensive tests were added to verify:
- Basic reordering functionality (up/down movement)
- Boundary conditions (first/last channels)
- Permission checks (non-admins cannot reorder)
- Ordering persistence across server restarts
- Correct broadcasting of changes to channel members
Migration Strategy
Existing channels are assigned initial channel_order values based on their current alphabetical sorting to maintain the familiar order users expect:
UPDATE channels
SET channel_order = (
SELECT ROW_NUMBER() OVER (
PARTITION BY parent_path
ORDER BY name, id
)
FROM channels c2
WHERE c2.id = channels.id
);
Future Enhancements
While this PR provides basic reordering functionality, potential future improvements could include:
- Drag-and-drop reordering in the UI
- Bulk reordering operations
- Custom sorting strategies (by activity, creation date, etc.)
Checklist
- [x] Database migration included
- [x] Tests added for new functionality
- [x] Keybindings work on macOS and Linux
- [x] Permissions properly enforced
- [x] Error handling implemented throughout
- [x] Manual testing completed
- [x] Documentation updated
Squawk Report
š 1 violations across 1 file(s)
crates/collab/migrations/20250530175450_add_channel_order.sql
-- Add channel_order column to channels table with default value
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;
-- Update channel_order for existing channels using ROW_NUMBER for deterministic ordering
UPDATE channels
SET channel_order = (
SELECT ROW_NUMBER() OVER (
PARTITION BY parent_path
ORDER BY name, id
)
FROM channels c2
WHERE c2.id = channels.id
);
-- Create index for efficient ordering queries
CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
š Rule Violations (1)
crates/collab/migrations/20250530175450_add_channel_order.sql:14:2: warning: require-concurrent-index-creation
14 | -- Create index for efficient ordering queries
15 | CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
note: Creating an index blocks writes.
help: Create the index CONCURRENTLY.
ā”ļø Powered by Squawk (0.26.0), a linter for PostgreSQL, focused on migrations