Clarify error message when attempting to delete channel with active participants (#54146)

Marco Groot created

Previous error message when trying to delete channel with active
participants in call:
<img width="2880" height="1800" alt="image"
src="https://github.com/user-attachments/assets/b11a0d75-438d-468f-8fe4-e0c249dd096c"
/>

After change (tested locally)

<img width="474" height="304" alt="image"
src="https://github.com/user-attachments/assets/1e887411-a851-4dc8-83dc-881f690c0ad9"
/>



Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE
https://github.com/zed-industries/zed/issues/53572

Release Notes:

- N/A or Added/Fixed/Improved ...
Added a clear error message upon trying to delete channel with active
participants

Change summary

crates/collab/src/db/queries/channels.rs                  | 11 +
crates/collab/tests/integration/db_tests/channel_tests.rs | 65 +++++++++
2 files changed, 76 insertions(+)

Detailed changes

crates/collab/src/db/queries/channels.rs 🔗

@@ -261,6 +261,17 @@ impl Database {
                 .chain(Some(channel_id))
                 .collect::<Vec<_>>();
 
+            let channel_has_active_participants = room_participant::Entity::find()
+                .inner_join(room::Entity)
+                .filter(room::Column::ChannelId.is_in(channels_to_remove.iter().copied()))
+                .count(&*tx)
+                .await?
+                > 0;
+
+            if channel_has_active_participants {
+                Err(anyhow!("can't delete channel while a call is in progress"))?;
+            }
+
             channel::Entity::delete_many()
                 .filter(channel::Column::Id.is_in(channels_to_remove.iter().copied()))
                 .exec(&*tx)

crates/collab/tests/integration/db_tests/channel_tests.rs 🔗

@@ -976,3 +976,68 @@ fn assert_channel_tree_order(actual: Vec<Channel>, expected: &[(ChannelId, &[Cha
         .collect::<HashSet<_>>();
     pretty_assertions::assert_eq!(actual, expected, "wrong channel ids and parent paths");
 }
+
+test_both_dbs!(
+    test_delete_channel_with_active_call,
+    test_delete_channel_with_active_call_postgres,
+    test_delete_channel_with_active_call_sqlite
+);
+
+async fn test_delete_channel_with_active_call(db: &Arc<Database>) {
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+
+    let user_1 = new_test_user(db, "user1@example.com").await;
+    let user_2 = new_test_user(db, "user2@example.com").await;
+
+    let parent_channel_id = db
+        .create_root_channel("parent_channel", user_1)
+        .await
+        .unwrap();
+    let nested_channel_id = db
+        .create_sub_channel("nested_channel", parent_channel_id, user_1)
+        .await
+        .unwrap();
+
+    db.invite_channel_member(parent_channel_id, user_2, user_1, ChannelRole::Member)
+        .await
+        .unwrap();
+
+    db.respond_to_channel_invite(parent_channel_id, user_2, true)
+        .await
+        .unwrap();
+
+    let connection_1 = ConnectionId { owner_id, id: 1 };
+    let connection_2 = ConnectionId { owner_id, id: 2 };
+
+    db.join_channel(parent_channel_id, user_1, connection_1)
+        .await
+        .unwrap();
+
+    db.join_channel(nested_channel_id, user_2, connection_2)
+        .await
+        .unwrap();
+
+    // Delete fails - participants in both parent and nested calls
+    let err = db
+        .delete_channel(parent_channel_id, user_1)
+        .await
+        .unwrap_err()
+        .to_string();
+    assert!(err.contains("call is in progress"), "{err}");
+
+    // Delete fails - participants in nested calls
+    db.leave_room(connection_2).await.unwrap();
+    let err = db
+        .delete_channel(parent_channel_id, user_1)
+        .await
+        .unwrap_err()
+        .to_string();
+    assert!(err.contains("call is in progress"), "{err}");
+
+    // Delete succeeds - no participants in calls
+    db.leave_room(connection_1).await.unwrap();
+    db.delete_channel(parent_channel_id, user_1).await.unwrap();
+
+    assert!(db.get_channel(parent_channel_id, user_1).await.is_err());
+    assert!(db.get_channel(parent_channel_id, user_2).await.is_err());
+}