Fix panic when following due to disconnected channel notes views (#3124)

Max Brunsfeld created

In addition to fixing a panic, this makes it slightly more convenient to
re-open disconnected channel notes views. I didn't make it automatic,
but it will at least replace the previous, disconnected view.

Release Notes:

- Fixed a crash that sometimes occurred when following someone with a
disconnected channel notes view open.

Change summary

crates/channel/src/channel_buffer.rs |  4 ++
crates/channel/src/channel_store.rs  | 24 +++++++++++---
crates/collab_ui/src/channel_view.rs | 49 ++++++++++++++++++++++-------
3 files changed, 59 insertions(+), 18 deletions(-)

Detailed changes

crates/channel/src/channel_buffer.rs 🔗

@@ -99,6 +99,10 @@ impl ChannelBuffer {
         }))
     }
 
+    pub fn remote_id(&self, cx: &AppContext) -> u64 {
+        self.buffer.read(cx).remote_id()
+    }
+
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
         &self.user_store
     }

crates/channel/src/channel_store.rs 🔗

@@ -114,12 +114,21 @@ impl ChannelStore {
         let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
             while let Some(status) = connection_status.next().await {
                 let this = this.upgrade(&cx)?;
+                match status {
+                    client::Status::Connected { .. } => {
+                        this.update(&mut cx, |this, cx| this.handle_connect(cx))
+                            .await
+                            .log_err()?;
+                    }
+                    client::Status::SignedOut | client::Status::UpgradeRequired => {
+                        this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
+                    }
+                    _ => {
+                        this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
+                    }
+                }
                 if status.is_connected() {
-                    this.update(&mut cx, |this, cx| this.handle_connect(cx))
-                        .await
-                        .log_err()?;
                 } else {
-                    this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
                 }
             }
             Some(())
@@ -823,7 +832,7 @@ impl ChannelStore {
         })
     }
 
-    fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
+    fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
         self.channel_index.clear();
         self.channel_invitations.clear();
         self.channel_participants.clear();
@@ -834,7 +843,10 @@ impl ChannelStore {
 
         self.disconnect_channel_buffers_task.get_or_insert_with(|| {
             cx.spawn_weak(|this, mut cx| async move {
-                cx.background().timer(RECONNECT_TIMEOUT).await;
+                if wait_for_reconnect {
+                    cx.background().timer(RECONNECT_TIMEOUT).await;
+                }
+
                 if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| {
                         for (_, buffer) in this.opened_buffers.drain() {

crates/collab_ui/src/channel_view.rs 🔗

@@ -24,7 +24,7 @@ use workspace::{
     item::{FollowableItem, Item, ItemHandle},
     register_followable_item,
     searchable::SearchableItemHandle,
-    ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
+    ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
 };
 
 actions!(channel_view, [Deploy]);
@@ -93,15 +93,36 @@ impl ChannelView {
             }
 
             pane.update(&mut cx, |pane, cx| {
-                pane.items_of_type::<Self>()
-                    .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
-                    .unwrap_or_else(|| {
-                        cx.add_view(|cx| {
-                            let mut this = Self::new(project, channel_store, channel_buffer, cx);
-                            this.acknowledge_buffer_version(cx);
-                            this
-                        })
-                    })
+                let buffer_id = channel_buffer.read(cx).remote_id(cx);
+
+                let existing_view = pane
+                    .items_of_type::<Self>()
+                    .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
+
+                // If this channel buffer is already open in this pane, just return it.
+                if let Some(existing_view) = existing_view.clone() {
+                    if existing_view.read(cx).channel_buffer == channel_buffer {
+                        return existing_view;
+                    }
+                }
+
+                let view = cx.add_view(|cx| {
+                    let mut this = Self::new(project, channel_store, channel_buffer, cx);
+                    this.acknowledge_buffer_version(cx);
+                    this
+                });
+
+                // If the pane contained a disconnected view for this channel buffer,
+                // replace that.
+                if let Some(existing_item) = existing_view {
+                    if let Some(ix) = pane.index_for_item(&existing_item) {
+                        pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
+                            .detach();
+                        pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
+                    }
+                }
+
+                view
             })
             .ok_or_else(|| anyhow!("pane was dropped"))
         })
@@ -285,10 +306,14 @@ impl FollowableItem for ChannelView {
     }
 
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
-        let channel = self.channel_buffer.read(cx).channel();
+        let channel_buffer = self.channel_buffer.read(cx);
+        if !channel_buffer.is_connected() {
+            return None;
+        }
+
         Some(proto::view::Variant::ChannelView(
             proto::view::ChannelView {
-                channel_id: channel.id,
+                channel_id: channel_buffer.channel().id,
                 editor: if let Some(proto::view::Variant::Editor(proto)) =
                     self.editor.read(cx).to_state_proto(cx)
                 {