ZIm/crates/proto/proto/channel.proto
Nathan Sobo 0a2186c87b
Add channel reordering functionality (#31833)
Release Notes:

- Added channel reordering for administrators (use `cmd-up` and
`cmd-down` on macOS or `ctrl-up` `ctrl-down` on 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:

```sql
-- 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:

```proto
// 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:

```rust
// 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:

```rust
// 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:

```json
// 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:

```rust
// 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:

```sql
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

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-06-04 16:56:33 +00:00

293 lines
5.6 KiB
Protocol Buffer

syntax = "proto3";
package zed.messages;
import "core.proto";
import "buffer.proto";
message Channel {
uint64 id = 1;
string name = 2;
ChannelVisibility visibility = 3;
int32 channel_order = 4;
repeated uint64 parent_path = 5;
}
enum ChannelVisibility {
Public = 0;
Members = 1;
}
message UpdateChannels {
repeated Channel channels = 1;
repeated uint64 delete_channels = 4;
repeated Channel channel_invitations = 5;
repeated uint64 remove_channel_invitations = 6;
repeated ChannelParticipants channel_participants = 7;
repeated ChannelMessageId latest_channel_message_ids = 8;
repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
reserved 10 to 15;
}
message UpdateUserChannels {
repeated ChannelMessageId observed_channel_message_id = 1;
repeated ChannelBufferVersion observed_channel_buffer_version = 2;
repeated ChannelMembership channel_memberships = 3;
}
message ChannelMembership {
uint64 channel_id = 1;
ChannelRole role = 2;
}
message ChannelMessageId {
uint64 channel_id = 1;
uint64 message_id = 2;
}
message ChannelPermission {
uint64 channel_id = 1;
ChannelRole role = 3;
}
message ChannelParticipants {
uint64 channel_id = 1;
repeated uint64 participant_user_ids = 2;
}
message JoinChannel {
uint64 channel_id = 1;
}
message DeleteChannel {
uint64 channel_id = 1;
}
message GetChannelMembers {
uint64 channel_id = 1;
string query = 2;
uint64 limit = 3;
}
message GetChannelMembersResponse {
repeated ChannelMember members = 1;
repeated User users = 2;
}
message ChannelMember {
uint64 user_id = 1;
Kind kind = 3;
ChannelRole role = 4;
enum Kind {
Member = 0;
Invitee = 1;
}
}
message SubscribeToChannels {}
message CreateChannel {
string name = 1;
optional uint64 parent_id = 2;
}
message CreateChannelResponse {
Channel channel = 1;
optional uint64 parent_id = 2;
}
message InviteChannelMember {
uint64 channel_id = 1;
uint64 user_id = 2;
ChannelRole role = 4;
}
message RemoveChannelMember {
uint64 channel_id = 1;
uint64 user_id = 2;
}
enum ChannelRole {
Admin = 0;
Member = 1;
Guest = 2;
Banned = 3;
Talker = 4;
}
message SetChannelMemberRole {
uint64 channel_id = 1;
uint64 user_id = 2;
ChannelRole role = 3;
}
message SetChannelVisibility {
uint64 channel_id = 1;
ChannelVisibility visibility = 2;
}
message RenameChannel {
uint64 channel_id = 1;
string name = 2;
}
message RenameChannelResponse {
Channel channel = 1;
}
message JoinChannelChat {
uint64 channel_id = 1;
}
message JoinChannelChatResponse {
repeated ChannelMessage messages = 1;
bool done = 2;
}
message LeaveChannelChat {
uint64 channel_id = 1;
}
message SendChannelMessage {
uint64 channel_id = 1;
string body = 2;
Nonce nonce = 3;
repeated ChatMention mentions = 4;
optional uint64 reply_to_message_id = 5;
}
message RemoveChannelMessage {
uint64 channel_id = 1;
uint64 message_id = 2;
}
message UpdateChannelMessage {
uint64 channel_id = 1;
uint64 message_id = 2;
Nonce nonce = 4;
string body = 5;
repeated ChatMention mentions = 6;
}
message AckChannelMessage {
uint64 channel_id = 1;
uint64 message_id = 2;
}
message SendChannelMessageResponse {
ChannelMessage message = 1;
}
message ChannelMessageSent {
uint64 channel_id = 1;
ChannelMessage message = 2;
}
message ChannelMessageUpdate {
uint64 channel_id = 1;
ChannelMessage message = 2;
}
message GetChannelMessages {
uint64 channel_id = 1;
uint64 before_message_id = 2;
}
message GetChannelMessagesResponse {
repeated ChannelMessage messages = 1;
bool done = 2;
}
message GetChannelMessagesById {
repeated uint64 message_ids = 1;
}
message MoveChannel {
uint64 channel_id = 1;
uint64 to = 2;
}
message ReorderChannel {
uint64 channel_id = 1;
enum Direction {
Up = 0;
Down = 1;
}
Direction direction = 2;
}
message JoinChannelBuffer {
uint64 channel_id = 1;
}
message ChannelBufferVersion {
uint64 channel_id = 1;
repeated VectorClockEntry version = 2;
uint64 epoch = 3;
}
message UpdateChannelBufferCollaborators {
uint64 channel_id = 1;
repeated Collaborator collaborators = 2;
}
message UpdateChannelBuffer {
uint64 channel_id = 1;
repeated Operation operations = 2;
}
message ChannelMessage {
uint64 id = 1;
string body = 2;
uint64 timestamp = 3;
uint64 sender_id = 4;
Nonce nonce = 5;
repeated ChatMention mentions = 6;
optional uint64 reply_to_message_id = 7;
optional uint64 edited_at = 8;
}
message ChatMention {
Range range = 1;
uint64 user_id = 2;
}
message RejoinChannelBuffers {
repeated ChannelBufferVersion buffers = 1;
}
message RejoinChannelBuffersResponse {
repeated RejoinedChannelBuffer buffers = 1;
}
message AckBufferOperation {
uint64 buffer_id = 1;
uint64 epoch = 2;
repeated VectorClockEntry version = 3;
}
message JoinChannelBufferResponse {
uint64 buffer_id = 1;
uint32 replica_id = 2;
string base_text = 3;
repeated Operation operations = 4;
repeated Collaborator collaborators = 5;
uint64 epoch = 6;
}
message RejoinedChannelBuffer {
uint64 channel_id = 1;
repeated VectorClockEntry version = 2;
repeated Operation operations = 3;
repeated Collaborator collaborators = 4;
}
message LeaveChannelBuffer {
uint64 channel_id = 1;
}
message RespondToChannelInvite {
uint64 channel_id = 1;
bool accept = 2;
}