Get more of chat panel compiling, but lots of todos

Nathan Sobo created

Change summary

crates/channel2/src/channel_store.rs                    |   11 
crates/channel2/src/channel_store/channel_index.rs      |    8 
crates/collab2/src/tests/channel_tests.rs               |   64 
crates/collab2/src/tests/random_channel_buffer_tests.rs |    8 
crates/collab_ui2/src/chat_panel.rs                     | 1548 +++++-----
crates/collab_ui2/src/chat_panel/message_editor.rs      |  225 
crates/collab_ui2/src/collab_panel.rs                   |    4 
crates/collab_ui2/src/collab_ui.rs                      |    7 
crates/gpui2/src/app.rs                                 |    5 
crates/gpui2/src/elements/list.rs                       |   37 
crates/gpui2/src/gpui2.rs                               |   62 
crates/gpui2/src/shared_string.rs                       |  101 
12 files changed, 1,073 insertions(+), 1,007 deletions(-)

Detailed changes

crates/channel2/src/channel_store.rs ๐Ÿ”—

@@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
 use db::RELEASE_CHANNEL;
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
+    WeakModel,
 };
 use rpc::{
     proto::{self, ChannelVisibility},
@@ -46,7 +47,7 @@ pub struct ChannelStore {
 #[derive(Clone, Debug, PartialEq)]
 pub struct Channel {
     pub id: ChannelId,
-    pub name: String,
+    pub name: SharedString,
     pub visibility: proto::ChannelVisibility,
     pub role: proto::ChannelRole,
     pub unseen_note_version: Option<(u64, clock::Global)>,
@@ -895,14 +896,16 @@ impl ChannelStore {
                 .channel_invitations
                 .binary_search_by_key(&channel.id, |c| c.id)
             {
-                Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
+                Ok(ix) => {
+                    Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
+                }
                 Err(ix) => self.channel_invitations.insert(
                     ix,
                     Arc::new(Channel {
                         id: channel.id,
                         visibility: channel.visibility(),
                         role: channel.role(),
-                        name: channel.name,
+                        name: channel.name.into(),
                         unseen_note_version: None,
                         unseen_message_id: None,
                         parent_path: channel.parent_path,

crates/channel2/src/channel_store/channel_index.rs ๐Ÿ”—

@@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
 
             existing_channel.visibility = channel_proto.visibility();
             existing_channel.role = channel_proto.role();
-            existing_channel.name = channel_proto.name;
+            existing_channel.name = channel_proto.name.into();
         } else {
             self.channels_by_id.insert(
                 channel_proto.id,
@@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
                     id: channel_proto.id,
                     visibility: channel_proto.visibility(),
                     role: channel_proto.role(),
-                    name: channel_proto.name,
+                    name: channel_proto.name.into(),
                     unseen_note_version: None,
                     unseen_message_id: None,
                     parent_path: channel_proto.parent_path,
@@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
     let (parent_path, name) = channels_by_id
         .get(&id)
         .map_or((&[] as &[_], None), |channel| {
-            (channel.parent_path.as_slice(), Some(channel.name.as_str()))
+            (channel.parent_path.as_slice(), Some(channel.name.as_ref()))
         });
     parent_path
         .iter()
-        .filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+        .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
         .chain(name)
 }
 

crates/collab2/src/tests/channel_tests.rs ๐Ÿ”—

@@ -7,7 +7,7 @@ use call::ActiveCall;
 use channel::{ChannelId, ChannelMembership, ChannelStore};
 use client::User;
 use futures::future::try_join_all;
-use gpui::{BackgroundExecutor, Model, TestAppContext};
+use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
 use rpc::{
     proto::{self, ChannelRole},
     RECEIVE_TIMEOUT,
@@ -46,13 +46,13 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 depth: 0,
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 depth: 1,
                 role: ChannelRole::Admin,
             },
@@ -92,7 +92,7 @@ async fn test_core_channels(
         cx_b,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Member,
         }],
@@ -140,13 +140,13 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 role: ChannelRole::Member,
                 depth: 0,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 role: ChannelRole::Member,
                 depth: 1,
             },
@@ -168,19 +168,19 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 role: ChannelRole::Member,
                 depth: 0,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 role: ChannelRole::Member,
                 depth: 1,
             },
             ExpectedChannel {
                 id: channel_c_id,
-                name: "channel-c".to_string(),
+                name: "channel-c".into(),
                 role: ChannelRole::Member,
                 depth: 2,
             },
@@ -211,19 +211,19 @@ async fn test_core_channels(
         &[
             ExpectedChannel {
                 id: channel_a_id,
-                name: "channel-a".to_string(),
+                name: "channel-a".into(),
                 depth: 0,
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_b_id,
-                name: "channel-b".to_string(),
+                name: "channel-b".into(),
                 depth: 1,
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 id: channel_c_id,
-                name: "channel-c".to_string(),
+                name: "channel-c".into(),
                 depth: 2,
                 role: ChannelRole::Admin,
             },
@@ -245,7 +245,7 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -255,7 +255,7 @@ async fn test_core_channels(
         cx_b,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -278,7 +278,7 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a".to_string(),
+            name: "channel-a".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -309,7 +309,7 @@ async fn test_core_channels(
         cx_a,
         &[ExpectedChannel {
             id: channel_a_id,
-            name: "channel-a-renamed".to_string(),
+            name: "channel-a-renamed".into(),
             depth: 0,
             role: ChannelRole::Admin,
         }],
@@ -418,7 +418,7 @@ async fn test_channel_room(
         cx_b,
         &[ExpectedChannel {
             id: zed_id,
-            name: "zed".to_string(),
+            name: "zed".into(),
             depth: 0,
             role: ChannelRole::Member,
         }],
@@ -680,7 +680,7 @@ async fn test_permissions_update_while_invited(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust".to_string(),
+            name: "rust".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -708,7 +708,7 @@ async fn test_permissions_update_while_invited(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust".to_string(),
+            name: "rust".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -747,7 +747,7 @@ async fn test_channel_rename(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust-archive".to_string(),
+            name: "rust-archive".into(),
             role: ChannelRole::Admin,
         }],
     );
@@ -759,7 +759,7 @@ async fn test_channel_rename(
         &[ExpectedChannel {
             depth: 0,
             id: rust_id,
-            name: "rust-archive".to_string(),
+            name: "rust-archive".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -888,7 +888,7 @@ async fn test_lost_channel_creation(
         &[ExpectedChannel {
             depth: 0,
             id: channel_id,
-            name: "x".to_string(),
+            name: "x".into(),
             role: ChannelRole::Member,
         }],
     );
@@ -912,13 +912,13 @@ async fn test_lost_channel_creation(
             ExpectedChannel {
                 depth: 0,
                 id: channel_id,
-                name: "x".to_string(),
+                name: "x".into(),
                 role: ChannelRole::Admin,
             },
             ExpectedChannel {
                 depth: 1,
                 id: subchannel_id,
-                name: "subchannel".to_string(),
+                name: "subchannel".into(),
                 role: ChannelRole::Admin,
             },
         ],
@@ -943,13 +943,13 @@ async fn test_lost_channel_creation(
             ExpectedChannel {
                 depth: 0,
                 id: channel_id,
-                name: "x".to_string(),
+                name: "x".into(),
                 role: ChannelRole::Member,
             },
             ExpectedChannel {
                 depth: 1,
                 id: subchannel_id,
-                name: "subchannel".to_string(),
+                name: "subchannel".into(),
                 role: ChannelRole::Member,
             },
         ],
@@ -1221,13 +1221,13 @@ async fn test_channel_membership_notifications(
             ExpectedChannel {
                 depth: 0,
                 id: zed_channel,
-                name: "zed".to_string(),
+                name: "zed".into(),
                 role: ChannelRole::Guest,
             },
             ExpectedChannel {
                 depth: 1,
                 id: vim_channel,
-                name: "vim".to_string(),
+                name: "vim".into(),
                 role: ChannelRole::Member,
             },
         ],
@@ -1250,13 +1250,13 @@ async fn test_channel_membership_notifications(
             ExpectedChannel {
                 depth: 0,
                 id: zed_channel,
-                name: "zed".to_string(),
+                name: "zed".into(),
                 role: ChannelRole::Guest,
             },
             ExpectedChannel {
                 depth: 1,
                 id: vim_channel,
-                name: "vim".to_string(),
+                name: "vim".into(),
                 role: ChannelRole::Guest,
             },
         ],
@@ -1476,7 +1476,7 @@ async fn test_channel_moving(
 struct ExpectedChannel {
     depth: usize,
     id: ChannelId,
-    name: String,
+    name: SharedString,
     role: ChannelRole,
 }
 
@@ -1515,7 +1515,7 @@ fn assert_channels(
                 .ordered_channels()
                 .map(|(depth, channel)| ExpectedChannel {
                     depth,
-                    name: channel.name.clone(),
+                    name: channel.name.clone().into(),
                     id: channel.id,
                     role: channel.role,
                 })

crates/collab2/src/tests/random_channel_buffer_tests.rs ๐Ÿ”—

@@ -3,7 +3,7 @@ use crate::db::ChannelRole;
 use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
 use anyhow::Result;
 use async_trait::async_trait;
-use gpui::{BackgroundExecutor, TestAppContext};
+use gpui::{BackgroundExecutor, SharedString, TestAppContext};
 use rand::prelude::*;
 use serde_derive::{Deserialize, Serialize};
 use std::{
@@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
 #[derive(Clone, Serialize, Deserialize)]
 enum ChannelBufferOperation {
     JoinChannelNotes {
-        channel_name: String,
+        channel_name: SharedString,
     },
     LeaveChannelNotes {
-        channel_name: String,
+        channel_name: SharedString,
     },
     EditChannelNotes {
-        channel_name: String,
+        channel_name: SharedString,
         edits: Vec<(Range<usize>, Arc<str>)>,
     },
     Noop,

crates/collab_ui2/src/chat_panel.rs ๐Ÿ”—

@@ -1,777 +1,785 @@
-// use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
-// use anyhow::Result;
-// use call::ActiveCall;
-// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
-// use client::Client;
-// use collections::HashMap;
-// use db::kvp::KEY_VALUE_STORE;
-// use editor::Editor;
-// use gpui::{
-//     actions, div, list, px, serde_json, AnyElement, AnyView, AppContext, AsyncAppContext, Div,
-//     Entity, EventEmitter, FocusableView, ListOffset, ListScrollHandle, Model, Orientation, Render,
-//     Subscription, Task, View, ViewContext, WeakView,
-// };
-// use language::LanguageRegistry;
-// use menu::Confirm;
-// use message_editor::MessageEditor;
-// use project::Fs;
-// use rich_text::RichText;
-// use serde::{Deserialize, Serialize};
-// use settings::{Settings, SettingsStore};
-// use std::sync::Arc;
-// use time::{OffsetDateTime, UtcOffset};
-// use ui::{h_stack, v_stack, Avatar, Button, Label};
-// use util::{ResultExt, TryFutureExt};
-// use workspace::{
-//     dock::{DockPosition, Panel},
-//     Workspace,
-// };
-
-// mod message_editor;
-
-// const MESSAGE_LOADING_THRESHOLD: usize = 50;
-// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
-
-// pub struct ChatPanel {
-//     client: Arc<Client>,
-//     channel_store: Model<ChannelStore>,
-//     languages: Arc<LanguageRegistry>,
-//     list_scroll: ListScrollHandle,
-//     active_chat: Option<(Model<ChannelChat>, Subscription)>,
-//     input_editor: View<MessageEditor>,
-//     local_timezone: UtcOffset,
-//     fs: Arc<dyn Fs>,
-//     width: Option<f32>,
-//     active: bool,
-//     pending_serialization: Task<Option<()>>,
-//     subscriptions: Vec<gpui::Subscription>,
-//     workspace: WeakView<Workspace>,
-//     is_scrolled_to_bottom: bool,
-//     has_focus: bool,
-//     markdown_data: HashMap<ChannelMessageId, RichText>,
-// }
-
-// #[derive(Serialize, Deserialize)]
-// struct SerializedChatPanel {
-//     width: Option<f32>,
-// }
-
-// #[derive(Debug)]
-// pub enum Event {
-//     DockPositionChanged,
-//     Focus,
-//     Dismissed,
-// }
-
-// actions!(LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall);
+use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
+use anyhow::Result;
+use call::ActiveCall;
+use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+use client::Client;
+use collections::HashMap;
+use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use gpui::{
+    actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+    Div, EventEmitter, FocusableView, ListState, Model, Render, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView,
+};
+use language::LanguageRegistry;
+use menu::Confirm;
+use message_editor::MessageEditor;
+use project::Fs;
+use rich_text::RichText;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use time::{OffsetDateTime, UtcOffset};
+use ui::prelude::WindowContext;
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel, PanelEvent},
+    Workspace,
+};
+
+mod message_editor;
+
+const MESSAGE_LOADING_THRESHOLD: usize = 50;
+const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+
+pub struct ChatPanel {
+    client: Arc<Client>,
+    channel_store: Model<ChannelStore>,
+    languages: Arc<LanguageRegistry>,
+    message_list: ListState,
+    active_chat: Option<(Model<ChannelChat>, Subscription)>,
+    input_editor: View<MessageEditor>,
+    local_timezone: UtcOffset,
+    fs: Arc<dyn Fs>,
+    width: Option<f32>,
+    active: bool,
+    pending_serialization: Task<Option<()>>,
+    subscriptions: Vec<gpui::Subscription>,
+    workspace: WeakView<Workspace>,
+    is_scrolled_to_bottom: bool,
+    has_focus: bool,
+    markdown_data: HashMap<ChannelMessageId, RichText>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedChatPanel {
+    width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
+
+actions!(LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall);
 
 // pub fn init(cx: &mut AppContext) {
-//     cx.add_action(ChatPanel::send);
-//     cx.add_action(ChatPanel::load_more_messages);
-//     cx.add_action(ChatPanel::open_notes);
-//     cx.add_action(ChatPanel::join_call);
-// }
-
-// impl ChatPanel {
-//     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
-//         let fs = workspace.app_state().fs.clone();
-//         let client = workspace.app_state().client.clone();
-//         let channel_store = ChannelStore::global(cx);
-//         let languages = workspace.app_state().languages.clone();
-
-//         let input_editor = cx.add_view(|cx| {
-//             MessageEditor::new(
-//                 languages.clone(),
-//                 channel_store.clone(),
-//                 cx.add_view(|cx| Editor::auto_height(4, cx)),
-//                 cx,
-//             )
-//         });
-
-//         let workspace_handle = workspace.weak_handle();
-
-//         // let channel_select = cx.add_view(|cx| {
-//         //     let channel_store = channel_store.clone();
-//         //     let workspace = workspace_handle.clone();
-//         //     Select::new(0, cx, {
-//         //         move |ix, item_type, is_hovered, cx| {
-//         //             Self::render_channel_name(
-//         //                 &channel_store,
-//         //                 ix,
-//         //                 item_type,
-//         //                 is_hovered,
-//         //                 workspace,
-//         //                 cx,
-//         //             )
-//         //         }
-//         //     })
-//         //     .with_style(move |cx| {
-//         //         let style = &cx.theme().chat_panel.channel_select;
-//         //         SelectStyle {
-//         //             header: Default::default(),
-//         //             menu: style.menu,
-//         //         }
-//         //     })
-//         // });
-
-//         // let mut message_list = ListState::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
-//         //     this.render_message(ix, cx)
-//         // });
-//         // message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
-//         //     if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
-//         //         this.load_more_messages(cx);
-//         //     }
-//         //     this.is_scrolled_to_bottom = event.visible_range.end == event.count;
-//         // }));
-
-//         cx.add_view(|cx| {
-//             let mut this = Self {
-//                 fs,
-//                 client,
-//                 channel_store,
-//                 languages,
-//                 list_scroll: ListScrollHandle::new(),
-//                 active_chat: Default::default(),
-//                 pending_serialization: Task::ready(None),
-//                 input_editor,
-//                 local_timezone: cx.platform().local_timezone(),
-//                 has_focus: false,
-//                 subscriptions: Vec::new(),
-//                 workspace: workspace_handle,
-//                 is_scrolled_to_bottom: true,
-//                 active: false,
-//                 width: None,
-//                 markdown_data: Default::default(),
-//             };
-
-//             let mut old_dock_position = this.position(cx);
-//             this.subscriptions
-//                 .push(
-//                     cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-//                         let new_dock_position = this.position(cx);
-//                         if new_dock_position != old_dock_position {
-//                             old_dock_position = new_dock_position;
-//                             cx.emit(Event::DockPositionChanged);
-//                         }
-//                         cx.notify();
-//                     }),
-//                 );
-
-//             this.update_channel_count(cx);
-//             cx.observe(&this.channel_store, |this, _, cx| {
-//                 this.update_channel_count(cx)
-//             })
-//             .detach();
-
-//             cx.observe(&this.channel_select, |this, channel_select, cx| {
-//                 let selected_ix = channel_select.read(cx).selected_index();
-
-//                 let selected_channel_id = this
-//                     .channel_store
-//                     .read(cx)
-//                     .channel_at(selected_ix)
-//                     .map(|e| e.id);
-//                 if let Some(selected_channel_id) = selected_channel_id {
-//                     this.select_channel(selected_channel_id, None, cx)
-//                         .detach_and_log_err(cx);
-//                 }
-//             })
-//             .detach();
-
-//             this
-//         })
-//     }
-
-//     pub fn is_scrolled_to_bottom(&self) -> bool {
-//         self.is_scrolled_to_bottom
-//     }
-
-//     pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
-//         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
-//     }
-
-//     pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
-//         cx.spawn(|mut cx| async move {
-//             let serialized_panel = if let Some(panel) = cx
-//                 .background()
-//                 .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
-//                 .await
-//                 .log_err()
-//                 .flatten()
-//             {
-//                 Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
-//             } else {
-//                 None
-//             };
-
-//             workspace.update(&mut cx, |workspace, cx| {
-//                 let panel = Self::new(workspace, cx);
-//                 if let Some(serialized_panel) = serialized_panel {
-//                     panel.update(cx, |panel, cx| {
-//                         panel.width = serialized_panel.width;
-//                         cx.notify();
-//                     });
-//                 }
-//                 panel
-//             })
-//         })
-//     }
-
-//     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-//         let width = self.width;
-//         self.pending_serialization = cx.background().spawn(
-//             async move {
-//                 KEY_VALUE_STORE
-//                     .write_kvp(
-//                         CHAT_PANEL_KEY.into(),
-//                         serde_json::to_string(&SerializedChatPanel { width })?,
-//                     )
-//                     .await?;
-//                 anyhow::Ok(())
-//             }
-//             .log_err(),
-//         );
-//     }
-
-//     fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
-//         let channel_count = self.channel_store.read(cx).channel_count();
-//         self.channel_select.update(cx, |select, cx| {
-//             select.set_item_count(channel_count, cx);
-//         });
-//     }
-
-//     fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
-//         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
-//             let channel_id = chat.read(cx).channel_id;
-//             {
-//                 self.markdown_data.clear();
-//                 let chat = chat.read(cx);
-//                 self.message_list.reset(chat.message_count());
-
-//                 let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
-//                 self.input_editor.update(cx, |editor, cx| {
-//                     editor.set_channel(channel_id, channel_name, cx);
-//                 });
-//             };
-//             let subscription = cx.subscribe(&chat, Self::channel_did_change);
-//             self.active_chat = Some((chat, subscription));
-//             self.acknowledge_last_message(cx);
-//             self.channel_select.update(cx, |select, cx| {
-//                 if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
-//                     select.set_selected_index(ix, cx);
-//                 }
-//             });
-//             cx.notify();
-//         }
-//     }
-
-//     fn channel_did_change(
-//         &mut self,
-//         _: Model<ChannelChat>,
-//         event: &ChannelChatEvent,
-//         cx: &mut ViewContext<Self>,
-//     ) {
-//         match event {
-//             ChannelChatEvent::MessagesUpdated {
-//                 old_range,
-//                 new_count,
-//             } => {
-//                 self.message_list.splice(old_range.clone(), *new_count);
-//                 if self.active {
-//                     self.acknowledge_last_message(cx);
-//                 }
-//             }
-//             ChannelChatEvent::NewMessage {
-//                 channel_id,
-//                 message_id,
-//             } => {
-//                 if !self.active {
-//                     self.channel_store.update(cx, |store, cx| {
-//                         store.new_message(*channel_id, *message_id, cx)
-//                     })
-//                 }
-//             }
-//         }
-//         cx.notify();
-//     }
-
-//     fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
-//         if self.active && self.is_scrolled_to_bottom {
-//             if let Some((chat, _)) = &self.active_chat {
-//                 chat.update(cx, |chat, cx| {
-//                     chat.acknowledge_last_message(cx);
-//                 });
-//             }
-//         }
-//     }
-
-//     fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
-//         v_stack()
-//             .child(Label::new(
-//                 self.active_chat.map_or(Default::default(), |c| {
-//                     c.0.read(cx).channel(cx)?.name.into()
-//                 }),
-//             ))
-//             .child(self.render_active_channel_messages(cx))
-//             .child(self.input_editor.to_any())
-//             .into_any()
-//     }
-
-//     fn render_active_channel_messages(&self, cx: &mut ViewContext<Self>) -> AnyElement {
-//         if self.active_chat.is_some() {
-//             list(
-//                 Orientation::Bottom,
-//                 10.,
-//                 cx.listener(move |this, ix, cx| this.render_message(ix, cx)),
-//             )
-//             .into_any_element()
-//         } else {
-//             div().into_any_element()
-//         }
-//     }
-
-//     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let (message, is_continuation, is_last, is_admin) = self
-//             .active_chat
-//             .as_ref()
-//             .unwrap()
-//             .0
-//             .update(cx, |active_chat, cx| {
-//                 let is_admin = self
-//                     .channel_store
-//                     .read(cx)
-//                     .is_channel_admin(active_chat.channel_id);
-
-//                 let last_message = active_chat.message(ix.saturating_sub(1));
-//                 let this_message = active_chat.message(ix).clone();
-//                 let is_continuation = last_message.id != this_message.id
-//                     && this_message.sender.id == last_message.sender.id;
-
-//                 if let ChannelMessageId::Saved(id) = this_message.id {
-//                     if this_message
-//                         .mentions
-//                         .iter()
-//                         .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
-//                     {
-//                         active_chat.acknowledge_message(id);
-//                     }
-//                 }
-
-//                 (
-//                     this_message,
-//                     is_continuation,
-//                     active_chat.message_count() == ix + 1,
-//                     is_admin,
-//                 )
-//             });
-
-//         let is_pending = message.is_pending();
-//         let text = self.markdown_data.entry(message.id).or_insert_with(|| {
-//             Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
-//         });
-
-//         let now = OffsetDateTime::now_utc();
-
-//         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-//         let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
-//             (message.id, belongs_to_user || is_admin)
-//         {
-//             Some(id)
-//         } else {
-//             None
-//         };
-
-//         if is_continuation {
-//             h_stack()
-//                 .child(text.element(cx))
-//                 .child(render_remove(message_id_to_remove, cx))
-//                 .mb_1()
-//                 .into_any()
-//         } else {
-//             v_stack()
-//                 .child(
-//                     h_stack()
-//                         .child(Avatar::data(message.sender.avatar.clone()))
-//                         .child(Label::new(message.sender.github_login.clone()))
-//                         .child(
-//                             Label::new(format_timestamp(
-//                                 message.timestamp,
-//                                 now,
-//                                 self.local_timezone,
-//                             ))
-//                             .flex(1., true),
-//                         )
-//                         .child(render_remove(message_id_to_remove, cx))
-//                         .align_children_center(),
-//                 )
-//                 .child(
-//                     h_stack()
-//                         .child(text.element(cx))
-//                         .child(render_remove(None, cx)),
-//                 )
-//                 .mb_1()
-//                 .into_any()
-//         }
-//     }
-
-//     fn render_markdown_with_mentions(
-//         language_registry: &Arc<LanguageRegistry>,
-//         current_user_id: u64,
-//         message: &channel::ChannelMessage,
-//     ) -> RichText {
-//         let mentions = message
-//             .mentions
-//             .iter()
-//             .map(|(range, user_id)| rich_text::Mention {
-//                 range: range.clone(),
-//                 is_self_mention: *user_id == current_user_id,
-//             })
-//             .collect::<Vec<_>>();
-
-//         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
-//     }
-
-//     // fn render_channel_name(
-//     //     channel_store: &Model<ChannelStore>,
-//     //     ix: usize,
-//     //     item_type: ItemType,
-//     //     is_hovered: bool,
-//     //     workspace: WeakView<Workspace>,
-//     //     cx: &mut ViewContext<Select>,
-//     // ) -> AnyElement<Select> {
-//     //     let theme = theme::current(cx);
-//     //     let tooltip_style = &theme.tooltip;
-//     //     let theme = &theme.chat_panel;
-//     //     let style = match (&item_type, is_hovered) {
-//     //         (ItemType::Header, _) => &theme.channel_select.header,
-//     //         (ItemType::Selected, _) => &theme.channel_select.active_item,
-//     //         (ItemType::Unselected, false) => &theme.channel_select.item,
-//     //         (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
-//     //     };
-
-//     //     let channel = &channel_store.read(cx).channel_at(ix).unwrap();
-//     //     let channel_id = channel.id;
-
-//     //     let mut row = Flex::row()
-//     //         .with_child(
-//     //             Label::new("#".to_string(), style.hash.text.clone())
-//     //                 .contained()
-//     //                 .with_style(style.hash.container),
-//     //         )
-//     //         .with_child(Label::new(channel.name.clone(), style.name.clone()));
-
-//     //     if matches!(item_type, ItemType::Header) {
-//     //         row.add_children([
-//     //             MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
-//     //                 render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
-//     //             })
-//     //             .on_click(MouseButton::Left, move |_, _, cx| {
-//     //                 if let Some(workspace) = workspace.upgrade(cx) {
-//     //                     ChannelView::open(channel_id, workspace, cx).detach();
-//     //                 }
-//     //             })
-//     //             .with_tooltip::<OpenChannelNotes>(
-//     //                 channel_id as usize,
-//     //                 "Open Notes",
-//     //                 Some(Box::new(OpenChannelNotes)),
-//     //                 tooltip_style.clone(),
-//     //                 cx,
-//     //             )
-//     //             .flex_float(),
-//     //             MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
-//     //                 render_icon_button(
-//     //                     theme.icon_button.style_for(mouse_state),
-//     //                     "icons/speaker-loud.svg",
-//     //                 )
-//     //             })
-//     //             .on_click(MouseButton::Left, move |_, _, cx| {
-//     //                 ActiveCall::global(cx)
-//     //                     .update(cx, |call, cx| call.join_channel(channel_id, cx))
-//     //                     .detach_and_log_err(cx);
-//     //             })
-//     //             .with_tooltip::<ActiveCall>(
-//     //                 channel_id as usize,
-//     //                 "Join Call",
-//     //                 Some(Box::new(JoinCall)),
-//     //                 tooltip_style.clone(),
-//     //                 cx,
-//     //             )
-//     //             .flex_float(),
-//     //         ]);
-//     //     }
-
-//     //     row.align_children_center()
-//     //         .contained()
-//     //         .with_style(style.container)
-//     //         .into_any()
-//     // }
-
-//     fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         enum SignInPromptLabel {}
-
-//         Button::new("sign-in", "Sign in to use chat")
-//             .on_click(move |_, this, cx| {
-//                 let client = this.client.clone();
-//                 cx.spawn(|this, mut cx| async move {
-//                     if client
-//                         .authenticate_and_connect(true, &cx)
-//                         .log_err()
-//                         .await
-//                         .is_some()
-//                     {
-//                         this.update(&mut cx, |this, cx| {
-//                             if cx.handle().is_focused(cx) {
-//                                 cx.focus(&this.input_editor);
-//                             }
-//                         })
-//                         .ok();
-//                     }
-//                 })
-//                 .detach();
-//             })
-//             .aligned()
-//             .into_any()
-//     }
-
-//     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = self.active_chat.as_ref() {
-//             let message = self
-//                 .input_editor
-//                 .update(cx, |editor, cx| editor.take_message(cx));
-
-//             if let Some(task) = chat
-//                 .update(cx, |chat, cx| chat.send_message(message, cx))
-//                 .log_err()
-//             {
-//                 task.detach();
-//             }
-//         }
-//     }
-
-//     fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = self.active_chat.as_ref() {
-//             chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
-//         }
-//     }
-
-//     fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = self.active_chat.as_ref() {
-//             chat.update(cx, |channel, cx| {
-//                 if let Some(task) = channel.load_more_messages(cx) {
-//                     task.detach();
-//                 }
-//             })
-//         }
-//     }
-
-//     pub fn select_channel(
-//         &mut self,
-//         selected_channel_id: u64,
-//         scroll_to_message_id: Option<u64>,
-//         cx: &mut ViewContext<ChatPanel>,
-//     ) -> Task<Result<()>> {
-//         let open_chat = self
-//             .active_chat
-//             .as_ref()
-//             .and_then(|(chat, _)| {
-//                 (chat.read(cx).channel_id == selected_channel_id)
-//                     .then(|| Task::ready(anyhow::Ok(chat.clone())))
-//             })
-//             .unwrap_or_else(|| {
-//                 self.channel_store.update(cx, |store, cx| {
-//                     store.open_channel_chat(selected_channel_id, cx)
-//                 })
-//             });
-
-//         cx.spawn(|this, mut cx| async move {
-//             let chat = open_chat.await?;
-//             this.update(&mut cx, |this, cx| {
-//                 this.set_active_chat(chat.clone(), cx);
-//             })?;
-
-//             if let Some(message_id) = scroll_to_message_id {
-//                 if let Some(item_ix) =
-//                     ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
-//                         .await
-//                 {
-//                     this.update(&mut cx, |this, cx| {
-//                         if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-//                             this.list_scroll.scroll_to(ListOffset {
-//                                 item_ix,
-//                                 offset_in_item: px(0.0),
-//                             });
-//                             cx.notify();
-//                         }
-//                     })?;
-//                 }
-//             }
-
-//             Ok(())
-//         })
-//     }
-
-//     fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = &self.active_chat {
-//             let channel_id = chat.read(cx).channel_id;
-//             if let Some(workspace) = self.workspace.upgrade(cx) {
-//                 ChannelView::open(channel_id, workspace, cx).detach();
-//             }
-//         }
-//     }
-
-//     fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
-//         if let Some((chat, _)) = &self.active_chat {
-//             let channel_id = chat.read(cx).channel_id;
-//             ActiveCall::global(cx)
-//                 .update(cx, |call, cx| call.join_channel(channel_id, cx))
-//                 .detach_and_log_err(cx);
-//         }
-//     }
-// }
-
-// fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
-//     enum DeleteMessage {}
-
-//     message_id_to_remove
-//         .map(|id| {
-//             MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
-//                 let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
-//                 render_icon_button(button_style, "icons/x.svg")
-//                     .aligned()
-//                     .into_any()
-//             })
-//             .with_padding(Padding::uniform(2.))
-//             .with_cursor_style(CursorStyle::PointingHand)
-//             .on_click(MouseButton::Left, move |_, this, cx| {
-//                 this.remove_message(id, cx);
-//             })
-//             .flex_float()
-//             .into_any()
-//         })
-//         .unwrap_or_else(|| {
-//             let style = theme.chat_panel.icon_button.default;
-
-//             Empty::new()
-//                 .constrained()
-//                 .with_width(style.icon_width)
-//                 .aligned()
-//                 .constrained()
-//                 .with_width(style.button_width)
-//                 .with_height(style.button_width)
-//                 .contained()
-//                 .with_uniform_padding(2.)
-//                 .flex_float()
-//                 .into_any()
-//         })
-// }
-
-// impl EventEmitter<Event> for ChatPanel {}
-
-// impl Render for ChatPanel {
-//     type Element = Div;
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-//         div()
-//             .child(if self.client.user_id().is_some() {
-//                 self.render_channel(cx)
-//             } else {
-//                 self.render_sign_in_prompt(cx)
-//             })
-//             .min_w(px(150.))
-//     }
-// }
-
-// impl FocusableView for ChatPanel {
-//     fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-//         self.input_editor.read(cx).focus_handle(cx)
-//     }
-// }
-
-// impl Panel for ChatPanel {
-//     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-//         ChatPanelSettings::get_global(cx).dock
-//     }
-
-//     fn position_is_valid(&self, position: DockPosition) -> bool {
-//         matches!(position, DockPosition::Left | DockPosition::Right)
-//     }
-
-//     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-//         settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
-//             settings.dock = Some(position)
-//         });
-//     }
-
-//     fn size(&self, cx: &gpui::WindowContext) -> f32 {
-//         self.width
-//             .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
-//     }
-
-//     fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
-//         self.width = size;
-//         self.serialize(cx);
-//         cx.notify();
-//     }
-
-//     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-//         self.active = active;
-//         if active {
-//             self.acknowledge_last_message(cx);
-//             if !is_channels_feature_enabled(cx) {
-//                 cx.emit(Event::Dismissed);
-//             }
-//         }
-//     }
-
-//     fn persistent_name() -> &'static str {
-//         todo!()
-//     }
-
-//     fn icon(&self, cx: &ui::prelude::WindowContext) -> Option<ui::Icon> {
-//         Some(ui::Icon::MessageBubbles)
-//     }
-
-//     fn toggle_action(&self) -> Box<dyn gpui::Action> {
-//         todo!()
-//     }
-// }
-
-// fn format_timestamp(
-//     mut timestamp: OffsetDateTime,
-//     mut now: OffsetDateTime,
-//     local_timezone: UtcOffset,
-// ) -> String {
-//     timestamp = timestamp.to_offset(local_timezone);
-//     now = now.to_offset(local_timezone);
-
-//     let today = now.date();
-//     let date = timestamp.date();
-//     let mut hour = timestamp.hour();
-//     let mut part = "am";
-//     if hour > 12 {
-//         hour -= 12;
-//         part = "pm";
-//     }
-//     if date == today {
-//         format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
-//     } else if date.next_day() == Some(today) {
-//         format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
-//     } else {
-//         format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-//     }
+// cx.add_action(ChatPanel::send);
+// cx.add_action(ChatPanel::load_more_messages);
+// cx.add_action(ChatPanel::open_notes);
+// cx.add_action(ChatPanel::join_call);
 // }
 
-// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
-//     Svg::new(svg_path)
-//         .with_color(style.color)
-//         .constrained()
-//         .with_width(style.icon_width)
-//         .aligned()
-//         .constrained()
-//         .with_width(style.button_width)
-//         .with_height(style.button_width)
-//         .contained()
-//         .with_style(style.container)
+impl ChatPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        let fs = workspace.app_state().fs.clone();
+        let client = workspace.app_state().client.clone();
+        let channel_store = ChannelStore::global(cx);
+        let languages = workspace.app_state().languages.clone();
+
+        let input_editor = cx.build_view(|cx| {
+            MessageEditor::new(
+                languages.clone(),
+                channel_store.clone(),
+                cx.build_view(|cx| Editor::auto_height(4, cx)),
+                cx,
+            )
+        });
+
+        let workspace_handle = workspace.weak_handle();
+
+        // let channel_select = cx.build_view(|cx| {
+        //     let channel_store = channel_store.clone();
+        //     let workspace = workspace_handle.clone();
+        //     Select::new(0, cx, {
+        //         move |ix, item_type, is_hovered, cx| {
+        //             Self::render_channel_name(
+        //                 &channel_store,
+        //                 ix,
+        //                 item_type,
+        //                 is_hovered,
+        //                 workspace,
+        //                 cx,
+        //             )
+        //         }
+        //     })
+        //     .with_style(move |cx| {
+        //         let style = &cx.theme().chat_panel.channel_select;
+        //         SelectStyle {
+        //             header: Default::default(),
+        //             menu: style.menu,
+        //         }
+        //     })
+        // });
+
+        cx.build_view(|cx| {
+            let view: View<ChatPanel> = cx.view().clone();
+            let message_list =
+                ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
+                    view.update(cx, |view, cx| view.render_message(ix, cx))
+                });
+
+            // message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
+            //     if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
+            //         this.load_more_messages(cx);
+            //     }
+            //     this.is_scrolled_to_bottom = event.visible_range.end == event.count;
+            // }));
+
+            let mut this = Self {
+                fs,
+                client,
+                channel_store,
+                languages,
+                message_list,
+                active_chat: Default::default(),
+                pending_serialization: Task::ready(None),
+                input_editor,
+                local_timezone: cx.local_timezone(),
+                has_focus: false,
+                subscriptions: Vec::new(),
+                workspace: workspace_handle,
+                is_scrolled_to_bottom: true,
+                active: false,
+                width: None,
+                markdown_data: Default::default(),
+            };
+
+            let mut old_dock_position = this.position(cx);
+            this.subscriptions.push(cx.observe_global::<SettingsStore>(
+                move |this: &mut Self, cx| {
+                    let new_dock_position = this.position(cx);
+                    if new_dock_position != old_dock_position {
+                        old_dock_position = new_dock_position;
+                        cx.emit(Event::DockPositionChanged);
+                    }
+                    cx.notify();
+                },
+            ));
+
+            // this.update_channel_count(cx);
+            // cx.observe(&this.channel_store, |this, _, cx| {
+            //     this.update_channel_count(cx)
+            // })
+            // .detach();
+
+            // cx.observe(&this.channel_select, |this, channel_select, cx| {
+            //     let selected_ix = channel_select.read(cx).selected_index();
+
+            //     let selected_channel_id = this
+            //         .channel_store
+            //         .read(cx)
+            //         .channel_at(selected_ix)
+            //         .map(|e| e.id);
+            //     if let Some(selected_channel_id) = selected_channel_id {
+            //         this.select_channel(selected_channel_id, None, cx)
+            //             .detach_and_log_err(cx);
+            //     }
+            // })
+            // .detach();
+
+            this
+        })
+    }
+
+    pub fn is_scrolled_to_bottom(&self) -> bool {
+        self.is_scrolled_to_bottom
+    }
+
+    pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
+        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+    }
+
+    pub fn load(
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background_executor()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = Self::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background_executor().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        CHAT_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedChatPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    // fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
+    //     let channel_count = self.channel_store.read(cx).channel_count();
+    //     self.channel_select.update(cx, |select, cx| {
+    //         select.set_item_count(channel_count, cx);
+    //     });
+    // }
+
+    fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
+        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
+            let channel_id = chat.read(cx).channel_id;
+            {
+                self.markdown_data.clear();
+                let chat = chat.read(cx);
+                self.message_list.reset(chat.message_count());
+
+                let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
+                self.input_editor.update(cx, |editor, cx| {
+                    editor.set_channel(channel_id, channel_name, cx);
+                });
+            };
+            let subscription = cx.subscribe(&chat, Self::channel_did_change);
+            self.active_chat = Some((chat, subscription));
+            self.acknowledge_last_message(cx);
+            // self.channel_select.update(cx, |select, cx| {
+            //     if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
+            //         select.set_selected_index(ix, cx);
+            //     }
+            // });
+            cx.notify();
+        }
+    }
+
+    fn channel_did_change(
+        &mut self,
+        _: Model<ChannelChat>,
+        event: &ChannelChatEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            ChannelChatEvent::MessagesUpdated {
+                old_range,
+                new_count,
+            } => {
+                self.message_list.splice(old_range.clone(), *new_count);
+                if self.active {
+                    self.acknowledge_last_message(cx);
+                }
+            }
+            ChannelChatEvent::NewMessage {
+                channel_id,
+                message_id,
+            } => {
+                if !self.active {
+                    self.channel_store.update(cx, |store, cx| {
+                        store.new_message(*channel_id, *message_id, cx)
+                    })
+                }
+            }
+        }
+        cx.notify();
+    }
+
+    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
+        if self.active && self.is_scrolled_to_bottom {
+            if let Some((chat, _)) = &self.active_chat {
+                chat.update(cx, |chat, cx| {
+                    chat.acknowledge_last_message(cx);
+                });
+            }
+        }
+    }
+
+    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        todo!()
+        // v_stack()
+        //     .child(Label::new(
+        //         self.active_chat.map_or(Default::default(), |c| {
+        //             c.0.read(cx).channel(cx)?.name.clone()
+        //         }),
+        //     ))
+        //     .child(self.render_active_channel_messages(cx))
+        //     .child(self.input_editor.clone())
+        //     .into_any()
+    }
+
+    fn render_active_channel_messages(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        if self.active_chat.is_some() {
+            list(self.message_list.clone()).into_any_element()
+        } else {
+            div().into_any_element()
+        }
+    }
+
+    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
+        todo!()
+        // let (message, is_continuation, is_last, is_admin) = self
+        //     .active_chat
+        //     .as_ref()
+        //     .unwrap()
+        //     .0
+        //     .update(cx, |active_chat, cx| {
+        //         let is_admin = self
+        //             .channel_store
+        //             .read(cx)
+        //             .is_channel_admin(active_chat.channel_id);
+
+        //         let last_message = active_chat.message(ix.saturating_sub(1));
+        //         let this_message = active_chat.message(ix).clone();
+        //         let is_continuation = last_message.id != this_message.id
+        //             && this_message.sender.id == last_message.sender.id;
+
+        //         if let ChannelMessageId::Saved(id) = this_message.id {
+        //             if this_message
+        //                 .mentions
+        //                 .iter()
+        //                 .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
+        //             {
+        //                 active_chat.acknowledge_message(id);
+        //             }
+        //         }
+
+        //         (
+        //             this_message,
+        //             is_continuation,
+        //             active_chat.message_count() == ix + 1,
+        //             is_admin,
+        //         )
+        //     });
+
+        // let is_pending = message.is_pending();
+        // let text = self.markdown_data.entry(message.id).or_insert_with(|| {
+        //     Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
+        // });
+
+        // let now = OffsetDateTime::now_utc();
+
+        // let belongs_to_user = Some(message.sender.id) == self.client.user_id();
+        // let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
+        //     (message.id, belongs_to_user || is_admin)
+        // {
+        //     Some(id)
+        // } else {
+        //     None
+        // };
+
+        // if is_continuation {
+        //     h_stack()
+        //         .child(text.element(cx))
+        //         .child(render_remove(message_id_to_remove, cx))
+        //         .mb_1()
+        //         .into_any()
+        // } else {
+        //     v_stack()
+        //         .child(
+        //             h_stack()
+        //                 .child(Avatar::data(message.sender.avatar.clone()))
+        //                 .child(Label::new(message.sender.github_login.clone()))
+        //                 .child(
+        //                     Label::new(format_timestamp(
+        //                         message.timestamp,
+        //                         now,
+        //                         self.local_timezone,
+        //                     ))
+        //                     .flex(1., true),
+        //                 )
+        //                 .child(render_remove(message_id_to_remove, cx))
+        //                 .align_children_center(),
+        //         )
+        //         .child(
+        //             h_stack()
+        //                 .child(text.element(cx))
+        //                 .child(render_remove(None, cx)),
+        //         )
+        //         .mb_1()
+        //         .into_any()
+        // }
+    }
+
+    fn render_markdown_with_mentions(
+        language_registry: &Arc<LanguageRegistry>,
+        current_user_id: u64,
+        message: &channel::ChannelMessage,
+    ) -> RichText {
+        let mentions = message
+            .mentions
+            .iter()
+            .map(|(range, user_id)| rich_text::Mention {
+                range: range.clone(),
+                is_self_mention: *user_id == current_user_id,
+            })
+            .collect::<Vec<_>>();
+
+        rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
+    }
+
+    // fn render_channel_name(
+    //     channel_store: &Model<ChannelStore>,
+    //     ix: usize,
+    //     item_type: ItemType,
+    //     is_hovered: bool,
+    //     workspace: WeakView<Workspace>,
+    //     cx: &mut ViewContext<Select>,
+    // ) -> AnyElement<Select> {
+    //     let theme = theme::current(cx);
+    //     let tooltip_style = &theme.tooltip;
+    //     let theme = &theme.chat_panel;
+    //     let style = match (&item_type, is_hovered) {
+    //         (ItemType::Header, _) => &theme.channel_select.header,
+    //         (ItemType::Selected, _) => &theme.channel_select.active_item,
+    //         (ItemType::Unselected, false) => &theme.channel_select.item,
+    //         (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
+    //     };
+
+    //     let channel = &channel_store.read(cx).channel_at(ix).unwrap();
+    //     let channel_id = channel.id;
+
+    //     let mut row = Flex::row()
+    //         .with_child(
+    //             Label::new("#".to_string(), style.hash.text.clone())
+    //                 .contained()
+    //                 .with_style(style.hash.container),
+    //         )
+    //         .with_child(Label::new(channel.name.clone(), style.name.clone()));
+
+    //     if matches!(item_type, ItemType::Header) {
+    //         row.add_children([
+    //             MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
+    //                 render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
+    //             })
+    //             .on_click(MouseButton::Left, move |_, _, cx| {
+    //                 if let Some(workspace) = workspace.upgrade(cx) {
+    //                     ChannelView::open(channel_id, workspace, cx).detach();
+    //                 }
+    //             })
+    //             .with_tooltip::<OpenChannelNotes>(
+    //                 channel_id as usize,
+    //                 "Open Notes",
+    //                 Some(Box::new(OpenChannelNotes)),
+    //                 tooltip_style.clone(),
+    //                 cx,
+    //             )
+    //             .flex_float(),
+    //             MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
+    //                 render_icon_button(
+    //                     theme.icon_button.style_for(mouse_state),
+    //                     "icons/speaker-loud.svg",
+    //                 )
+    //             })
+    //             .on_click(MouseButton::Left, move |_, _, cx| {
+    //                 ActiveCall::global(cx)
+    //                     .update(cx, |call, cx| call.join_channel(channel_id, cx))
+    //                     .detach_and_log_err(cx);
+    //             })
+    //             .with_tooltip::<ActiveCall>(
+    //                 channel_id as usize,
+    //                 "Join Call",
+    //                 Some(Box::new(JoinCall)),
+    //                 tooltip_style.clone(),
+    //                 cx,
+    //             )
+    //             .flex_float(),
+    //         ]);
+    //     }
+
+    //     row.align_children_center()
+    //         .contained()
+    //         .with_style(style.container)
+    //         .into_any()
+    // }
+
+    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        todo!()
+        // enum SignInPromptLabel {}
+
+        // Button::new("sign-in", "Sign in to use chat")
+        //     .on_click(move |_, this, cx| {
+        //         let client = this.client.clone();
+        //         cx.spawn(|this, mut cx| async move {
+        //             if client
+        //                 .authenticate_and_connect(true, &cx)
+        //                 .log_err()
+        //                 .await
+        //                 .is_some()
+        //             {
+        //                 this.update(&mut cx, |this, cx| {
+        //                     if cx.handle().is_focused(cx) {
+        //                         cx.focus(&this.input_editor);
+        //                     }
+        //                 })
+        //                 .ok();
+        //             }
+        //         })
+        //         .detach();
+        //     })
+        //     .aligned()
+        //     .into_any()
+    }
+
+    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            let message = self
+                .input_editor
+                .update(cx, |editor, cx| editor.take_message(cx));
+
+            if let Some(task) = chat
+                .update(cx, |chat, cx| chat.send_message(message, cx))
+                .log_err()
+            {
+                task.detach();
+            }
+        }
+    }
+
+    fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
+        }
+    }
+
+    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |channel, cx| {
+                if let Some(task) = channel.load_more_messages(cx) {
+                    task.detach();
+                }
+            })
+        }
+    }
+
+    pub fn select_channel(
+        &mut self,
+        selected_channel_id: u64,
+        scroll_to_message_id: Option<u64>,
+        cx: &mut ViewContext<ChatPanel>,
+    ) -> Task<Result<()>> {
+        todo!()
+        // let open_chat = self
+        //     .active_chat
+        //     .as_ref()
+        //     .and_then(|(chat, _)| {
+        //         (chat.read(cx).channel_id == selected_channel_id)
+        //             .then(|| Task::ready(anyhow::Ok(chat.clone())))
+        //     })
+        //     .unwrap_or_else(|| {
+        //         self.channel_store.update(cx, |store, cx| {
+        //             store.open_channel_chat(selected_channel_id, cx)
+        //         })
+        //     });
+
+        // cx.spawn(|this, mut cx| async move {
+        //     let chat = open_chat.await?;
+        //     this.update(&mut cx, |this, cx| {
+        //         this.set_active_chat(chat.clone(), cx);
+        //     })?;
+
+        //     if let Some(message_id) = scroll_to_message_id {
+        //         if let Some(item_ix) =
+        //             ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
+        //                 .await
+        //         {
+        //             this.update(&mut cx, |this, cx| {
+        //                 if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
+        //                     this.message_list.scroll_to(ListOffset {
+        //                         item_ix,
+        //                         offset_in_item: px(0.0),
+        //                     });
+        //                     cx.notify();
+        //                 }
+        //             })?;
+        //         }
+        //     }
+
+        //     Ok(())
+        // })
+    }
+
+    fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = &self.active_chat {
+            let channel_id = chat.read(cx).channel_id;
+            if let Some(workspace) = self.workspace.upgrade() {
+                ChannelView::open(channel_id, workspace, cx).detach();
+            }
+        }
+    }
+
+    fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = &self.active_chat {
+            let channel_id = chat.read(cx).channel_id;
+            ActiveCall::global(cx)
+                .update(cx, |call, cx| call.join_channel(channel_id, cx))
+                .detach_and_log_err(cx);
+        }
+    }
+}
+
+fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
+    todo!()
+    // enum DeleteMessage {}
+
+    // message_id_to_remove
+    //     .map(|id| {
+    //         MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
+    //             let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
+    //             render_icon_button(button_style, "icons/x.svg")
+    //                 .aligned()
+    //                 .into_any()
+    //         })
+    //         .with_padding(Padding::uniform(2.))
+    //         .with_cursor_style(CursorStyle::PointingHand)
+    //         .on_click(MouseButton::Left, move |_, this, cx| {
+    //             this.remove_message(id, cx);
+    //         })
+    //         .flex_float()
+    //         .into_any()
+    //     })
+    //     .unwrap_or_else(|| {
+    //         let style = theme.chat_panel.icon_button.default;
+
+    //         Empty::new()
+    //             .constrained()
+    //             .with_width(style.icon_width)
+    //             .aligned()
+    //             .constrained()
+    //             .with_width(style.button_width)
+    //             .with_height(style.button_width)
+    //             .contained()
+    //             .with_uniform_padding(2.)
+    //             .flex_float()
+    //             .into_any()
+    //     })
+}
+
+impl EventEmitter<Event> for ChatPanel {}
+
+impl Render for ChatPanel {
+    type Element = Div;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .child(if self.client.user_id().is_some() {
+                self.render_channel(cx)
+            } else {
+                self.render_sign_in_prompt(cx)
+            })
+            .min_w(px(150.))
+    }
+}
+
+impl FocusableView for ChatPanel {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.input_editor.read(cx).focus_handle(cx)
+    }
+}
+
+impl Panel for ChatPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        ChatPanelSettings::get_global(cx).dock
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
+            settings.dock = Some(position)
+        });
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+        self.active = active;
+        if active {
+            self.acknowledge_last_message(cx);
+            if !is_channels_feature_enabled(cx) {
+                cx.emit(Event::Dismissed);
+            }
+        }
+    }
+
+    fn persistent_name() -> &'static str {
+        todo!()
+    }
+
+    fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
+        Some(ui::Icon::MessageBubbles)
+    }
+
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        todo!()
+    }
+}
+
+impl EventEmitter<PanelEvent> for ChatPanel {}
+
+fn format_timestamp(
+    mut timestamp: OffsetDateTime,
+    mut now: OffsetDateTime,
+    local_timezone: UtcOffset,
+) -> String {
+    timestamp = timestamp.to_offset(local_timezone);
+    now = now.to_offset(local_timezone);
+
+    let today = now.date();
+    let date = timestamp.date();
+    let mut hour = timestamp.hour();
+    let mut part = "am";
+    if hour > 12 {
+        hour -= 12;
+        part = "pm";
+    }
+    if date == today {
+        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
+    } else if date.next_day() == Some(today) {
+        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
+    } else {
+        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+    }
+}
+
+// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
+//     todo!()
+//     // Svg::new(svg_path)
+//     //     .with_color(style.color)
+//     //     .constrained()
+//     //     .with_width(style.icon_width)
+//     //     .aligned()
+//     //     .constrained()
+//     //     .with_width(style.button_width)
+//     //     .with_height(style.button_width)
+//     //     .contained()
+//     //     .with_style(style.container)
 // }
 
 // #[cfg(test)]

crates/collab_ui2/src/chat_panel/message_editor.rs ๐Ÿ”—

@@ -2,7 +2,9 @@ use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
 use client::UserId;
 use collections::HashMap;
 use editor::{AnchorRangeExt, Editor};
-use gpui::{AnyView, AsyncAppContext, Model, Render, Task, View, ViewContext, WeakView};
+use gpui::{
+    AnyView, AsyncWindowContext, Model, Render, SharedString, Task, View, ViewContext, WeakView,
+};
 use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 use lazy_static::lazy_static;
 use project::search::SearchQuery;
@@ -46,15 +48,14 @@ impl MessageEditor {
         cx.subscribe(&buffer, Self::on_buffer_event).detach();
 
         let markdown = language_registry.language_for_name("Markdown");
-        cx.app_context()
-            .spawn(|mut cx| async move {
-                let markdown = markdown.await?;
-                buffer.update(&mut cx, |buffer, cx| {
-                    buffer.set_language(Some(markdown), cx)
-                });
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
+        cx.spawn(|_, mut cx| async move {
+            let markdown = markdown.await?;
+            buffer.update(&mut cx, |buffer, cx| {
+                buffer.set_language(Some(markdown), cx)
+            });
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
 
         Self {
             editor,
@@ -69,7 +70,7 @@ impl MessageEditor {
     pub fn set_channel(
         &mut self,
         channel_id: u64,
-        channel_name: Option<String>,
+        channel_name: Option<SharedString>,
         cx: &mut ViewContext<Self>,
     ) {
         self.editor.update(cx, |editor, cx| {
@@ -137,7 +138,9 @@ impl MessageEditor {
         if let language::Event::Reparsed | language::Event::Edited = event {
             let buffer = buffer.read(cx).snapshot();
             self.mentions_task = Some(cx.spawn(|this, cx| async move {
-                cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+                cx.background_executor()
+                    .timer(MENTIONS_DEBOUNCE_INTERVAL)
+                    .await;
                 Self::find_mentions(this, buffer, cx).await;
             }));
         }
@@ -146,10 +149,10 @@ impl MessageEditor {
     async fn find_mentions(
         this: WeakView<MessageEditor>,
         buffer: BufferSnapshot,
-        mut cx: AsyncAppContext,
+        mut cx: AsyncWindowContext,
     ) {
         let (buffer, ranges) = cx
-            .background()
+            .background_executor()
             .spawn(async move {
                 let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
                 (buffer, ranges)
@@ -186,6 +189,10 @@ impl MessageEditor {
         })
         .ok();
     }
+
+    pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
+        todo!()
+    }
 }
 
 impl Render for MessageEditor {
@@ -196,98 +203,98 @@ impl Render for MessageEditor {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use client::{Client, User, UserStore};
-    use gpui::{TestAppContext, WindowHandle};
-    use language::{Language, LanguageConfig};
-    use rpc::proto;
-    use settings::SettingsStore;
-    use util::{http::FakeHttpClient, test::marked_text_ranges};
-
-    #[gpui::test]
-    async fn test_message_editor(cx: &mut TestAppContext) {
-        let editor = init_test(cx);
-        let editor = editor.root(cx);
-
-        editor.update(cx, |editor, cx| {
-            editor.set_members(
-                vec![
-                    ChannelMembership {
-                        user: Arc::new(User {
-                            github_login: "a-b".into(),
-                            id: 101,
-                            avatar: None,
-                        }),
-                        kind: proto::channel_member::Kind::Member,
-                        role: proto::ChannelRole::Member,
-                    },
-                    ChannelMembership {
-                        user: Arc::new(User {
-                            github_login: "C_D".into(),
-                            id: 102,
-                            avatar: None,
-                        }),
-                        kind: proto::channel_member::Kind::Member,
-                        role: proto::ChannelRole::Member,
-                    },
-                ],
-                cx,
-            );
-
-            editor.editor.update(cx, |editor, cx| {
-                editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
-            });
-        });
-
-        cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
-
-        editor.update(cx, |editor, cx| {
-            let (text, ranges) = marked_text_ranges("Hello, ยซ@a-bยป! Have you met ยซ@C_Dยป?", false);
-            assert_eq!(
-                editor.take_message(cx),
-                MessageParams {
-                    text,
-                    mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-                }
-            );
-        });
-    }
-
-    fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
-        cx.foreground().forbid_parking();
-
-        cx.update(|cx| {
-            let http = FakeHttpClient::with_404_response();
-            let client = Client::new(http.clone(), cx);
-            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-            cx.set_global(SettingsStore::test(cx));
-            theme::init((), cx);
-            language::init(cx);
-            editor::init(cx);
-            client::init(&client, cx);
-            channel::init(&client, user_store, cx);
-        });
-
-        let language_registry = Arc::new(LanguageRegistry::test());
-        language_registry.add(Arc::new(Language::new(
-            LanguageConfig {
-                name: "Markdown".into(),
-                ..Default::default()
-            },
-            Some(tree_sitter_markdown::language()),
-        )));
-
-        let editor = cx.add_window(|cx| {
-            MessageEditor::new(
-                language_registry,
-                ChannelStore::global(cx),
-                cx.add_view(|cx| Editor::auto_height(4, cx)),
-                cx,
-            )
-        });
-        cx.foreground().run_until_parked();
-        editor
-    }
-}
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+//     use client::{Client, User, UserStore};
+//     use gpui::{TestAppContext, WindowHandle};
+//     use language::{Language, LanguageConfig};
+//     use rpc::proto;
+//     use settings::SettingsStore;
+//     use util::{http::FakeHttpClient, test::marked_text_ranges};
+
+//     #[gpui::test]
+//     async fn test_message_editor(cx: &mut TestAppContext) {
+//         let editor = init_test(cx);
+//         let editor = editor.root(cx);
+
+//         editor.update(cx, |editor, cx| {
+//             editor.set_members(
+//                 vec![
+//                     ChannelMembership {
+//                         user: Arc::new(User {
+//                             github_login: "a-b".into(),
+//                             id: 101,
+//                             avatar: None,
+//                         }),
+//                         kind: proto::channel_member::Kind::Member,
+//                         role: proto::ChannelRole::Member,
+//                     },
+//                     ChannelMembership {
+//                         user: Arc::new(User {
+//                             github_login: "C_D".into(),
+//                             id: 102,
+//                             avatar: None,
+//                         }),
+//                         kind: proto::channel_member::Kind::Member,
+//                         role: proto::ChannelRole::Member,
+//                     },
+//                 ],
+//                 cx,
+//             );
+
+//             editor.editor.update(cx, |editor, cx| {
+//                 editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
+//             });
+//         });
+
+//         cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+
+//         editor.update(cx, |editor, cx| {
+//             let (text, ranges) = marked_text_ranges("Hello, ยซ@a-bยป! Have you met ยซ@C_Dยป?", false);
+//             assert_eq!(
+//                 editor.take_message(cx),
+//                 MessageParams {
+//                     text,
+//                     mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+//                 }
+//             );
+//         });
+//     }
+
+//     fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
+//         cx.foreground().forbid_parking();
+
+//         cx.update(|cx| {
+//             let http = FakeHttpClient::with_404_response();
+//             let client = Client::new(http.clone(), cx);
+//             let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+//             cx.set_global(SettingsStore::test(cx));
+//             theme::init((), cx);
+//             language::init(cx);
+//             editor::init(cx);
+//             client::init(&client, cx);
+//             channel::init(&client, user_store, cx);
+//         });
+
+//         let language_registry = Arc::new(LanguageRegistry::test());
+//         language_registry.add(Arc::new(Language::new(
+//             LanguageConfig {
+//                 name: "Markdown".into(),
+//                 ..Default::default()
+//             },
+//             Some(tree_sitter_markdown::language()),
+//         )));
+
+//         let editor = cx.add_window(|cx| {
+//             MessageEditor::new(
+//                 language_registry,
+//                 ChannelStore::global(cx),
+//                 cx.add_view(|cx| Editor::auto_height(4, cx)),
+//                 cx,
+//             )
+//         });
+//         cx.foreground().run_until_parked();
+//         editor
+//     }
+// }

crates/collab_ui2/src/collab_panel.rs ๐Ÿ”—

@@ -852,7 +852,7 @@ impl CollabPanel {
                     .extend(channel_store.ordered_channels().enumerate().map(
                         |(ix, (_, channel))| StringMatchCandidate {
                             id: ix,
-                            string: channel.name.clone(),
+                            string: channel.name.clone().into(),
                             char_bag: channel.name.chars().collect(),
                         },
                     ));
@@ -2262,7 +2262,7 @@ impl CollabPanel {
                         }
                     };
 
-                    Some(channel.name.as_str())
+                    Some(channel.name.as_ref())
                 });
 
                 if let Some(name) = channel_name {

crates/collab_ui2/src/collab_ui.rs ๐Ÿ”—

@@ -12,6 +12,7 @@ use std::{rc::Rc, sync::Arc};
 use call::{report_call_event_for_room, ActiveCall, Room};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use gpui::{
     actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
     WindowKind, WindowOptions,
@@ -157,6 +158,6 @@ fn notification_window_options(
 //         .into_any()
 // }
 
-// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-//     cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-// }
+fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+}

crates/gpui2/src/app.rs ๐Ÿ”—

@@ -13,6 +13,7 @@ use smallvec::SmallVec;
 use smol::future::FutureExt;
 #[cfg(any(test, feature = "test-support"))]
 pub use test_context::*;
+use time::UtcOffset;
 
 use crate::{
     current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
@@ -536,6 +537,10 @@ impl AppContext {
         self.platform.restart()
     }
 
+    pub fn local_timezone(&self) -> UtcOffset {
+        self.platform.local_timezone()
+    }
+
     pub(crate) fn push_effect(&mut self, effect: Effect) {
         match &effect {
             Effect::Notify { emitter } => {

crates/gpui2/src/elements/list.rs ๐Ÿ”—

@@ -1,6 +1,6 @@
 use crate::{
     px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
-    Point, ScrollWheelEvent, Size, Style, StyleRefinement, ViewContext, WindowContext,
+    Point, ScrollWheelEvent, Size, Style, StyleRefinement, WindowContext,
 };
 use collections::VecDeque;
 use std::{cell::RefCell, ops::Range, rc::Rc};
@@ -26,14 +26,14 @@ struct StateInner {
     render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
     items: SumTree<ListItem>,
     logical_scroll_top: Option<ListOffset>,
-    orientation: Orientation,
+    alignment: ListAlignment,
     overdraw: Pixels,
     #[allow(clippy::type_complexity)]
     scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
 }
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum Orientation {
+pub enum ListAlignment {
     Top,
     Bottom,
 }
@@ -70,28 +70,23 @@ struct UnrenderedCount(usize);
 struct Height(Pixels);
 
 impl ListState {
-    pub fn new<F, V>(
+    pub fn new<F>(
         element_count: usize,
-        orientation: Orientation,
+        orientation: ListAlignment,
         overdraw: Pixels,
-        cx: &mut ViewContext<V>,
-        mut render_item: F,
+        render_item: F,
     ) -> Self
     where
-        F: 'static + FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement,
-        V: 'static,
+        F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
     {
         let mut items = SumTree::new();
         items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
-        let view = cx.view().clone();
         Self(Rc::new(RefCell::new(StateInner {
             last_layout_width: None,
-            render_item: Box::new(move |ix, cx| {
-                view.update(cx, |view, cx| render_item(view, ix, cx))
-            }),
+            render_item: Box::new(render_item),
             items,
             logical_scroll_top: None,
-            orientation,
+            alignment: orientation,
             overdraw,
             scroll_handler: None,
         })))
@@ -179,7 +174,7 @@ impl StateInner {
             .max(px(0.))
             .min(scroll_max);
 
-        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
+        if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
             self.logical_scroll_top = None;
         } else {
             let mut cursor = self.items.cursor::<ListItemSummary>();
@@ -208,12 +203,12 @@ impl StateInner {
 
     fn logical_scroll_top(&self) -> ListOffset {
         self.logical_scroll_top
-            .unwrap_or_else(|| match self.orientation {
-                Orientation::Top => ListOffset {
+            .unwrap_or_else(|| match self.alignment {
+                ListAlignment::Top => ListOffset {
                     item_ix: 0,
                     offset_in_item: px(0.),
                 },
-                Orientation::Bottom => ListOffset {
+                ListAlignment::Bottom => ListOffset {
                     item_ix: self.items.summary().count,
                     offset_in_item: px(0.),
                 },
@@ -344,12 +339,12 @@ impl Element for List {
                 offset_in_item: rendered_height - bounds.size.height,
             };
 
-            match state.orientation {
-                Orientation::Top => {
+            match state.alignment {
+                ListAlignment::Top => {
                     scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
                     state.logical_scroll_top = Some(scroll_top);
                 }
-                Orientation::Bottom => {
+                ListAlignment::Bottom => {
                     scroll_top = ListOffset {
                         item_ix: cursor.start().0,
                         offset_in_item: rendered_height - bounds.size.height,

crates/gpui2/src/gpui2.rs ๐Ÿ”—

@@ -1,6 +1,7 @@
 #[macro_use]
 mod action;
 mod app;
+
 mod assets;
 mod color;
 mod element;
@@ -15,6 +16,7 @@ mod keymap;
 mod platform;
 pub mod prelude;
 mod scene;
+mod shared_string;
 mod style;
 mod styled;
 mod subscription;
@@ -57,6 +59,7 @@ pub use scene::*;
 pub use serde;
 pub use serde_derive;
 pub use serde_json;
+pub use shared_string::*;
 pub use smallvec;
 pub use smol::Timer;
 pub use style::*;
@@ -76,6 +79,7 @@ use serde::{Deserialize, Serialize};
 use std::{
     any::{Any, TypeId},
     borrow::{Borrow, BorrowMut},
+    sync::Arc,
 };
 use taffy::TaffyLayoutEngine;
 
@@ -210,61 +214,3 @@ impl<T> Flatten<T> for Result<T> {
         self
     }
 }
-
-#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
-pub struct SharedString(ArcCow<'static, str>);
-
-impl Default for SharedString {
-    fn default() -> Self {
-        Self(ArcCow::Owned("".into()))
-    }
-}
-
-impl AsRef<str> for SharedString {
-    fn as_ref(&self) -> &str {
-        &self.0
-    }
-}
-
-impl Borrow<str> for SharedString {
-    fn borrow(&self) -> &str {
-        self.as_ref()
-    }
-}
-
-impl std::fmt::Debug for SharedString {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl std::fmt::Display for SharedString {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0.as_ref())
-    }
-}
-
-impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
-    fn from(value: T) -> Self {
-        Self(value.into())
-    }
-}
-
-impl Serialize for SharedString {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        serializer.serialize_str(self.as_ref())
-    }
-}
-
-impl<'de> Deserialize<'de> for SharedString {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        let s = String::deserialize(deserializer)?;
-        Ok(SharedString::from(s))
-    }
-}

crates/gpui2/src/shared_string.rs ๐Ÿ”—

@@ -0,0 +1,101 @@
+use derive_more::{Deref, DerefMut};
+use serde::{Deserialize, Serialize};
+use std::{borrow::Borrow, sync::Arc};
+use util::arc_cow::ArcCow;
+
+#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
+pub struct SharedString(ArcCow<'static, str>);
+
+impl Default for SharedString {
+    fn default() -> Self {
+        Self(ArcCow::Owned("".into()))
+    }
+}
+
+impl AsRef<str> for SharedString {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl Borrow<str> for SharedString {
+    fn borrow(&self) -> &str {
+        self.as_ref()
+    }
+}
+
+impl std::fmt::Debug for SharedString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl std::fmt::Display for SharedString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0.as_ref())
+    }
+}
+
+impl PartialEq<String> for SharedString {
+    fn eq(&self, other: &String) -> bool {
+        self.as_ref() == other
+    }
+}
+
+impl PartialEq<SharedString> for String {
+    fn eq(&self, other: &SharedString) -> bool {
+        self == other.as_ref()
+    }
+}
+
+impl PartialEq<str> for SharedString {
+    fn eq(&self, other: &str) -> bool {
+        self.as_ref() == other
+    }
+}
+
+impl<'a> PartialEq<&'a str> for SharedString {
+    fn eq(&self, other: &&'a str) -> bool {
+        self.as_ref() == *other
+    }
+}
+
+impl Into<Arc<str>> for SharedString {
+    fn into(self) -> Arc<str> {
+        match self.0 {
+            ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
+            ArcCow::Owned(owned) => owned.clone(),
+        }
+    }
+}
+
+impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
+    fn from(value: T) -> Self {
+        Self(value.into())
+    }
+}
+
+impl Into<String> for SharedString {
+    fn into(self) -> String {
+        self.0.to_string()
+    }
+}
+
+impl Serialize for SharedString {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(self.as_ref())
+    }
+}
+
+impl<'de> Deserialize<'de> for SharedString {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        Ok(SharedString::from(s))
+    }
+}