Cargo.lock 🔗
@@ -1864,6 +1864,7 @@ dependencies = [
"theme2",
"time",
"tree-sitter-markdown",
+ "ui2",
"util",
"workspace2",
"zed_actions2",
Conrad Irwin created
Cargo.lock | 1
Cargo.toml | 1
crates/collab_ui2/Cargo.toml | 1
crates/collab_ui2/src/channel_view.rs | 908 +-
crates/collab_ui2/src/chat_panel.rs | 1966 ++--
crates/collab_ui2/src/collab_panel.rs | 7096 ++++++++++----------
crates/collab_ui2/src/collab_titlebar_item.rs | 2549 +++---
crates/collab_ui2/src/collab_ui.rs | 262
crates/collab_ui2/src/face_pile.rs | 196
crates/collab_ui2/src/notification_panel.rs | 1768 ++--
crates/collab_ui2/src/notifications.rs | 18
crates/collab_ui2/src/panel_settings.rs | 14
crates/workspace2/src/workspace2.rs | 101
crates/zed2/src/main.rs | 2
crates/zed2/src/zed2.rs | 4
15 files changed, 7,447 insertions(+), 7,440 deletions(-)
@@ -1864,6 +1864,7 @@ dependencies = [
"theme2",
"time",
"tree-sitter-markdown",
+ "ui2",
"util",
"workspace2",
"zed_actions2",
@@ -18,6 +18,7 @@ members = [
"crates/collab",
"crates/collab2",
"crates/collab_ui",
+ "crates/collab_ui2",
"crates/collections",
"crates/command_palette",
"crates/command_palette2",
@@ -48,6 +48,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
theme = { package = "theme2", path = "../theme2" }
# theme_selector = { path = "../theme_selector" }
# vcs_menu = { path = "../vcs_menu" }
+ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" }
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
@@ -1,454 +1,454 @@
-use anyhow::{anyhow, Result};
-use call::report_call_event_for_channel;
-use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
-use client::{
- proto::{self, PeerId},
- Collaborator, ParticipantIndex,
-};
-use collections::HashMap;
-use editor::{CollaborationHub, Editor};
-use gpui::{
- actions,
- elements::{ChildView, Label},
- geometry::vector::Vector2F,
- AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
- ViewContext, ViewHandle,
-};
-use project::Project;
-use smallvec::SmallVec;
-use std::{
- any::{Any, TypeId},
- sync::Arc,
-};
-use util::ResultExt;
-use workspace::{
- item::{FollowableItem, Item, ItemEvent, ItemHandle},
- register_followable_item,
- searchable::SearchableItemHandle,
- ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
-};
-
-actions!(channel_view, [Deploy]);
-
-pub fn init(cx: &mut AppContext) {
- register_followable_item::<ChannelView>(cx)
-}
-
-pub struct ChannelView {
- pub editor: ViewHandle<Editor>,
- project: ModelHandle<Project>,
- channel_store: ModelHandle<ChannelStore>,
- channel_buffer: ModelHandle<ChannelBuffer>,
- remote_id: Option<ViewId>,
- _editor_event_subscription: Subscription,
-}
-
-impl ChannelView {
- pub fn open(
- channel_id: ChannelId,
- workspace: ViewHandle<Workspace>,
- cx: &mut AppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
- let pane = workspace.read(cx).active_pane().clone();
- let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
- cx.spawn(|mut cx| async move {
- let channel_view = channel_view.await?;
- pane.update(&mut cx, |pane, cx| {
- report_call_event_for_channel(
- "open channel notes",
- channel_id,
- &workspace.read(cx).app_state().client,
- cx,
- );
- pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
- });
- anyhow::Ok(channel_view)
- })
- }
-
- pub fn open_in_pane(
- channel_id: ChannelId,
- pane: ViewHandle<Pane>,
- workspace: ViewHandle<Workspace>,
- cx: &mut AppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
- let workspace = workspace.read(cx);
- let project = workspace.project().to_owned();
- let channel_store = ChannelStore::global(cx);
- let language_registry = workspace.app_state().languages.clone();
- let markdown = language_registry.language_for_name("Markdown");
- let channel_buffer =
- channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
-
- cx.spawn(|mut cx| async move {
- let channel_buffer = channel_buffer.await?;
- let markdown = markdown.await.log_err();
-
- channel_buffer.update(&mut cx, |buffer, cx| {
- buffer.buffer().update(cx, |buffer, cx| {
- buffer.set_language_registry(language_registry);
- if let Some(markdown) = markdown {
- buffer.set_language(Some(markdown), cx);
- }
- })
- });
-
- pane.update(&mut cx, |pane, cx| {
- let buffer_id = channel_buffer.read(cx).remote_id(cx);
-
- let existing_view = pane
- .items_of_type::<Self>()
- .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
-
- // If this channel buffer is already open in this pane, just return it.
- if let Some(existing_view) = existing_view.clone() {
- if existing_view.read(cx).channel_buffer == channel_buffer {
- return existing_view;
- }
- }
-
- let view = cx.add_view(|cx| {
- let mut this = Self::new(project, channel_store, channel_buffer, cx);
- this.acknowledge_buffer_version(cx);
- this
- });
-
- // If the pane contained a disconnected view for this channel buffer,
- // replace that.
- if let Some(existing_item) = existing_view {
- if let Some(ix) = pane.index_for_item(&existing_item) {
- pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
- .detach();
- pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
- }
- }
-
- view
- })
- .ok_or_else(|| anyhow!("pane was dropped"))
- })
- }
-
- pub fn new(
- project: ModelHandle<Project>,
- channel_store: ModelHandle<ChannelStore>,
- channel_buffer: ModelHandle<ChannelBuffer>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let buffer = channel_buffer.read(cx).buffer();
- let editor = cx.add_view(|cx| {
- let mut editor = Editor::for_buffer(buffer, None, cx);
- editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
- channel_buffer.clone(),
- )));
- editor.set_read_only(
- !channel_buffer
- .read(cx)
- .channel(cx)
- .is_some_and(|c| c.can_edit_notes()),
- );
- editor
- });
- let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
-
- cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
- .detach();
-
- Self {
- editor,
- project,
- channel_store,
- channel_buffer,
- remote_id: None,
- _editor_event_subscription,
- }
- }
-
- pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
- self.channel_buffer.read(cx).channel(cx)
- }
-
- fn handle_channel_buffer_event(
- &mut self,
- _: ModelHandle<ChannelBuffer>,
- event: &ChannelBufferEvent,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
- editor.set_read_only(true);
- cx.notify();
- }),
- ChannelBufferEvent::ChannelChanged => {
- self.editor.update(cx, |editor, cx| {
- editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
- cx.emit(editor::Event::TitleChanged);
- cx.notify()
- });
- }
- ChannelBufferEvent::BufferEdited => {
- if cx.is_self_focused() || self.editor.is_focused(cx) {
- self.acknowledge_buffer_version(cx);
- } else {
- self.channel_store.update(cx, |store, cx| {
- let channel_buffer = self.channel_buffer.read(cx);
- store.notes_changed(
- channel_buffer.channel_id,
- channel_buffer.epoch(),
- &channel_buffer.buffer().read(cx).version(),
- cx,
- )
- });
- }
- }
- ChannelBufferEvent::CollaboratorsChanged => {}
- }
- }
-
- fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
- self.channel_store.update(cx, |store, cx| {
- let channel_buffer = self.channel_buffer.read(cx);
- store.acknowledge_notes_version(
- channel_buffer.channel_id,
- channel_buffer.epoch(),
- &channel_buffer.buffer().read(cx).version(),
- cx,
- )
- });
- self.channel_buffer.update(cx, |buffer, cx| {
- buffer.acknowledge_buffer_version(cx);
- });
- }
-}
-
-impl Entity for ChannelView {
- type Event = editor::Event;
-}
-
-impl View for ChannelView {
- fn ui_name() -> &'static str {
- "ChannelView"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- ChildView::new(self.editor.as_any(), cx).into_any()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- if cx.is_self_focused() {
- self.acknowledge_buffer_version(cx);
- cx.focus(self.editor.as_any())
- }
- }
-}
-
-impl Item for ChannelView {
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a ViewHandle<Self>,
- _: &'a AppContext,
- ) -> Option<&'a AnyViewHandle> {
- if type_id == TypeId::of::<Self>() {
- Some(self_handle)
- } else if type_id == TypeId::of::<Editor>() {
- Some(&self.editor)
- } else {
- None
- }
- }
-
- fn tab_content<V: 'static>(
- &self,
- _: Option<usize>,
- style: &theme::Tab,
- cx: &gpui::AppContext,
- ) -> AnyElement<V> {
- let label = if let Some(channel) = self.channel(cx) {
- match (
- channel.can_edit_notes(),
- self.channel_buffer.read(cx).is_connected(),
- ) {
- (true, true) => format!("#{}", channel.name),
- (false, true) => format!("#{} (read-only)", channel.name),
- (_, false) => format!("#{} (disconnected)", channel.name),
- }
- } else {
- format!("channel notes (disconnected)")
- };
- Label::new(label, style.label.to_owned()).into_any()
- }
-
- fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
- Some(Self::new(
- self.project.clone(),
- self.channel_store.clone(),
- self.channel_buffer.clone(),
- cx,
- ))
- }
-
- fn is_singleton(&self, _cx: &AppContext) -> bool {
- false
- }
-
- fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
- self.editor
- .update(cx, |editor, cx| editor.navigate(data, cx))
- }
-
- fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| Item::deactivated(editor, cx))
- }
-
- fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
- }
-
- fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(self.editor.clone()))
- }
-
- fn show_toolbar(&self) -> bool {
- true
- }
-
- fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
- self.editor.read(cx).pixel_position_of_cursor(cx)
- }
-
- fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
- editor::Editor::to_item_events(event)
- }
-}
-
-impl FollowableItem for ChannelView {
- fn remote_id(&self) -> Option<workspace::ViewId> {
- self.remote_id
- }
-
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
- let channel_buffer = self.channel_buffer.read(cx);
- if !channel_buffer.is_connected() {
- return None;
- }
-
- Some(proto::view::Variant::ChannelView(
- proto::view::ChannelView {
- channel_id: channel_buffer.channel_id,
- editor: if let Some(proto::view::Variant::Editor(proto)) =
- self.editor.read(cx).to_state_proto(cx)
- {
- Some(proto)
- } else {
- None
- },
- },
- ))
- }
-
- fn from_state_proto(
- pane: ViewHandle<workspace::Pane>,
- workspace: ViewHandle<workspace::Workspace>,
- remote_id: workspace::ViewId,
- state: &mut Option<proto::view::Variant>,
- cx: &mut AppContext,
- ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
- let Some(proto::view::Variant::ChannelView(_)) = state else {
- return None;
- };
- let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
- unreachable!()
- };
-
- let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
-
- Some(cx.spawn(|mut cx| async move {
- let this = open.await?;
-
- let task = this
- .update(&mut cx, |this, cx| {
- this.remote_id = Some(remote_id);
-
- if let Some(state) = state.editor {
- Some(this.editor.update(cx, |editor, cx| {
- editor.apply_update_proto(
- &this.project,
- proto::update_view::Variant::Editor(proto::update_view::Editor {
- selections: state.selections,
- pending_selection: state.pending_selection,
- scroll_top_anchor: state.scroll_top_anchor,
- scroll_x: state.scroll_x,
- scroll_y: state.scroll_y,
- ..Default::default()
- }),
- cx,
- )
- }))
- } else {
- None
- }
- })
- .ok_or_else(|| anyhow!("window was closed"))?;
-
- if let Some(task) = task {
- task.await?;
- }
-
- Ok(this)
- }))
- }
-
- fn add_event_to_update_proto(
- &self,
- event: &Self::Event,
- update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
- ) -> bool {
- self.editor
- .read(cx)
- .add_event_to_update_proto(event, update, cx)
- }
-
- fn apply_update_proto(
- &mut self,
- project: &ModelHandle<Project>,
- message: proto::update_view::Variant,
- cx: &mut ViewContext<Self>,
- ) -> gpui::Task<anyhow::Result<()>> {
- self.editor.update(cx, |editor, cx| {
- editor.apply_update_proto(project, message, cx)
- })
- }
-
- fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
- self.editor.update(cx, |editor, cx| {
- editor.set_leader_peer_id(leader_peer_id, cx)
- })
- }
-
- fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
- Editor::should_unfollow_on_event(event, cx)
- }
-
- fn is_project_item(&self, _cx: &AppContext) -> bool {
- false
- }
-}
-
-struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
-
-impl CollaborationHub for ChannelBufferCollaborationHub {
- fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
- self.0.read(cx).collaborators()
- }
-
- fn user_participant_indices<'a>(
- &self,
- cx: &'a AppContext,
- ) -> &'a HashMap<u64, ParticipantIndex> {
- self.0.read(cx).user_store().read(cx).participant_indices()
- }
-}
+// use anyhow::{anyhow, Result};
+// use call::report_call_event_for_channel;
+// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
+// use client::{
+// proto::{self, PeerId},
+// Collaborator, ParticipantIndex,
+// };
+// use collections::HashMap;
+// use editor::{CollaborationHub, Editor};
+// use gpui::{
+// actions,
+// elements::{ChildView, Label},
+// geometry::vector::Vector2F,
+// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
+// ViewContext, ViewHandle,
+// };
+// use project::Project;
+// use smallvec::SmallVec;
+// use std::{
+// any::{Any, TypeId},
+// sync::Arc,
+// };
+// use util::ResultExt;
+// use workspace::{
+// item::{FollowableItem, Item, ItemEvent, ItemHandle},
+// register_followable_item,
+// searchable::SearchableItemHandle,
+// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
+// };
+
+// actions!(channel_view, [Deploy]);
+
+// pub fn init(cx: &mut AppContext) {
+// register_followable_item::<ChannelView>(cx)
+// }
+
+// pub struct ChannelView {
+// pub editor: ViewHandle<Editor>,
+// project: ModelHandle<Project>,
+// channel_store: ModelHandle<ChannelStore>,
+// channel_buffer: ModelHandle<ChannelBuffer>,
+// remote_id: Option<ViewId>,
+// _editor_event_subscription: Subscription,
+// }
+
+// impl ChannelView {
+// pub fn open(
+// channel_id: ChannelId,
+// workspace: ViewHandle<Workspace>,
+// cx: &mut AppContext,
+// ) -> Task<Result<ViewHandle<Self>>> {
+// let pane = workspace.read(cx).active_pane().clone();
+// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
+// cx.spawn(|mut cx| async move {
+// let channel_view = channel_view.await?;
+// pane.update(&mut cx, |pane, cx| {
+// report_call_event_for_channel(
+// "open channel notes",
+// channel_id,
+// &workspace.read(cx).app_state().client,
+// cx,
+// );
+// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
+// });
+// anyhow::Ok(channel_view)
+// })
+// }
+
+// pub fn open_in_pane(
+// channel_id: ChannelId,
+// pane: ViewHandle<Pane>,
+// workspace: ViewHandle<Workspace>,
+// cx: &mut AppContext,
+// ) -> Task<Result<ViewHandle<Self>>> {
+// let workspace = workspace.read(cx);
+// let project = workspace.project().to_owned();
+// let channel_store = ChannelStore::global(cx);
+// let language_registry = workspace.app_state().languages.clone();
+// let markdown = language_registry.language_for_name("Markdown");
+// let channel_buffer =
+// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+// cx.spawn(|mut cx| async move {
+// let channel_buffer = channel_buffer.await?;
+// let markdown = markdown.await.log_err();
+
+// channel_buffer.update(&mut cx, |buffer, cx| {
+// buffer.buffer().update(cx, |buffer, cx| {
+// buffer.set_language_registry(language_registry);
+// if let Some(markdown) = markdown {
+// buffer.set_language(Some(markdown), cx);
+// }
+// })
+// });
+
+// pane.update(&mut cx, |pane, cx| {
+// let buffer_id = channel_buffer.read(cx).remote_id(cx);
+
+// let existing_view = pane
+// .items_of_type::<Self>()
+// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
+
+// // If this channel buffer is already open in this pane, just return it.
+// if let Some(existing_view) = existing_view.clone() {
+// if existing_view.read(cx).channel_buffer == channel_buffer {
+// return existing_view;
+// }
+// }
+
+// let view = cx.add_view(|cx| {
+// let mut this = Self::new(project, channel_store, channel_buffer, cx);
+// this.acknowledge_buffer_version(cx);
+// this
+// });
+
+// // If the pane contained a disconnected view for this channel buffer,
+// // replace that.
+// if let Some(existing_item) = existing_view {
+// if let Some(ix) = pane.index_for_item(&existing_item) {
+// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
+// .detach();
+// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
+// }
+// }
+
+// view
+// })
+// .ok_or_else(|| anyhow!("pane was dropped"))
+// })
+// }
+
+// pub fn new(
+// project: ModelHandle<Project>,
+// channel_store: ModelHandle<ChannelStore>,
+// channel_buffer: ModelHandle<ChannelBuffer>,
+// cx: &mut ViewContext<Self>,
+// ) -> Self {
+// let buffer = channel_buffer.read(cx).buffer();
+// let editor = cx.add_view(|cx| {
+// let mut editor = Editor::for_buffer(buffer, None, cx);
+// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
+// channel_buffer.clone(),
+// )));
+// editor.set_read_only(
+// !channel_buffer
+// .read(cx)
+// .channel(cx)
+// .is_some_and(|c| c.can_edit_notes()),
+// );
+// editor
+// });
+// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+
+// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
+// .detach();
+
+// Self {
+// editor,
+// project,
+// channel_store,
+// channel_buffer,
+// remote_id: None,
+// _editor_event_subscription,
+// }
+// }
+
+// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
+// self.channel_buffer.read(cx).channel(cx)
+// }
+
+// fn handle_channel_buffer_event(
+// &mut self,
+// _: ModelHandle<ChannelBuffer>,
+// event: &ChannelBufferEvent,
+// cx: &mut ViewContext<Self>,
+// ) {
+// match event {
+// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
+// editor.set_read_only(true);
+// cx.notify();
+// }),
+// ChannelBufferEvent::ChannelChanged => {
+// self.editor.update(cx, |editor, cx| {
+// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
+// cx.emit(editor::Event::TitleChanged);
+// cx.notify()
+// });
+// }
+// ChannelBufferEvent::BufferEdited => {
+// if cx.is_self_focused() || self.editor.is_focused(cx) {
+// self.acknowledge_buffer_version(cx);
+// } else {
+// self.channel_store.update(cx, |store, cx| {
+// let channel_buffer = self.channel_buffer.read(cx);
+// store.notes_changed(
+// channel_buffer.channel_id,
+// channel_buffer.epoch(),
+// &channel_buffer.buffer().read(cx).version(),
+// cx,
+// )
+// });
+// }
+// }
+// ChannelBufferEvent::CollaboratorsChanged => {}
+// }
+// }
+
+// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
+// self.channel_store.update(cx, |store, cx| {
+// let channel_buffer = self.channel_buffer.read(cx);
+// store.acknowledge_notes_version(
+// channel_buffer.channel_id,
+// channel_buffer.epoch(),
+// &channel_buffer.buffer().read(cx).version(),
+// cx,
+// )
+// });
+// self.channel_buffer.update(cx, |buffer, cx| {
+// buffer.acknowledge_buffer_version(cx);
+// });
+// }
+// }
+
+// impl Entity for ChannelView {
+// type Event = editor::Event;
+// }
+
+// impl View for ChannelView {
+// fn ui_name() -> &'static str {
+// "ChannelView"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// ChildView::new(self.editor.as_any(), cx).into_any()
+// }
+
+// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+// if cx.is_self_focused() {
+// self.acknowledge_buffer_version(cx);
+// cx.focus(self.editor.as_any())
+// }
+// }
+// }
+
+// impl Item for ChannelView {
+// fn act_as_type<'a>(
+// &'a self,
+// type_id: TypeId,
+// self_handle: &'a ViewHandle<Self>,
+// _: &'a AppContext,
+// ) -> Option<&'a AnyViewHandle> {
+// if type_id == TypeId::of::<Self>() {
+// Some(self_handle)
+// } else if type_id == TypeId::of::<Editor>() {
+// Some(&self.editor)
+// } else {
+// None
+// }
+// }
+
+// fn tab_content<V: 'static>(
+// &self,
+// _: Option<usize>,
+// style: &theme::Tab,
+// cx: &gpui::AppContext,
+// ) -> AnyElement<V> {
+// let label = if let Some(channel) = self.channel(cx) {
+// match (
+// channel.can_edit_notes(),
+// self.channel_buffer.read(cx).is_connected(),
+// ) {
+// (true, true) => format!("#{}", channel.name),
+// (false, true) => format!("#{} (read-only)", channel.name),
+// (_, false) => format!("#{} (disconnected)", channel.name),
+// }
+// } else {
+// format!("channel notes (disconnected)")
+// };
+// Label::new(label, style.label.to_owned()).into_any()
+// }
+
+// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
+// Some(Self::new(
+// self.project.clone(),
+// self.channel_store.clone(),
+// self.channel_buffer.clone(),
+// cx,
+// ))
+// }
+
+// fn is_singleton(&self, _cx: &AppContext) -> bool {
+// false
+// }
+
+// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+// self.editor
+// .update(cx, |editor, cx| editor.navigate(data, cx))
+// }
+
+// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+// self.editor
+// .update(cx, |editor, cx| Item::deactivated(editor, cx))
+// }
+
+// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+// self.editor
+// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
+// }
+
+// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+// Some(Box::new(self.editor.clone()))
+// }
+
+// fn show_toolbar(&self) -> bool {
+// true
+// }
+
+// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+// self.editor.read(cx).pixel_position_of_cursor(cx)
+// }
+
+// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+// editor::Editor::to_item_events(event)
+// }
+// }
+
+// impl FollowableItem for ChannelView {
+// fn remote_id(&self) -> Option<workspace::ViewId> {
+// self.remote_id
+// }
+
+// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+// let channel_buffer = self.channel_buffer.read(cx);
+// if !channel_buffer.is_connected() {
+// return None;
+// }
+
+// Some(proto::view::Variant::ChannelView(
+// proto::view::ChannelView {
+// channel_id: channel_buffer.channel_id,
+// editor: if let Some(proto::view::Variant::Editor(proto)) =
+// self.editor.read(cx).to_state_proto(cx)
+// {
+// Some(proto)
+// } else {
+// None
+// },
+// },
+// ))
+// }
+
+// fn from_state_proto(
+// pane: ViewHandle<workspace::Pane>,
+// workspace: ViewHandle<workspace::Workspace>,
+// remote_id: workspace::ViewId,
+// state: &mut Option<proto::view::Variant>,
+// cx: &mut AppContext,
+// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+// let Some(proto::view::Variant::ChannelView(_)) = state else {
+// return None;
+// };
+// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
+// unreachable!()
+// };
+
+// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
+
+// Some(cx.spawn(|mut cx| async move {
+// let this = open.await?;
+
+// let task = this
+// .update(&mut cx, |this, cx| {
+// this.remote_id = Some(remote_id);
+
+// if let Some(state) = state.editor {
+// Some(this.editor.update(cx, |editor, cx| {
+// editor.apply_update_proto(
+// &this.project,
+// proto::update_view::Variant::Editor(proto::update_view::Editor {
+// selections: state.selections,
+// pending_selection: state.pending_selection,
+// scroll_top_anchor: state.scroll_top_anchor,
+// scroll_x: state.scroll_x,
+// scroll_y: state.scroll_y,
+// ..Default::default()
+// }),
+// cx,
+// )
+// }))
+// } else {
+// None
+// }
+// })
+// .ok_or_else(|| anyhow!("window was closed"))?;
+
+// if let Some(task) = task {
+// task.await?;
+// }
+
+// Ok(this)
+// }))
+// }
+
+// fn add_event_to_update_proto(
+// &self,
+// event: &Self::Event,
+// update: &mut Option<proto::update_view::Variant>,
+// cx: &AppContext,
+// ) -> bool {
+// self.editor
+// .read(cx)
+// .add_event_to_update_proto(event, update, cx)
+// }
+
+// fn apply_update_proto(
+// &mut self,
+// project: &ModelHandle<Project>,
+// message: proto::update_view::Variant,
+// cx: &mut ViewContext<Self>,
+// ) -> gpui::Task<anyhow::Result<()>> {
+// self.editor.update(cx, |editor, cx| {
+// editor.apply_update_proto(project, message, cx)
+// })
+// }
+
+// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+// self.editor.update(cx, |editor, cx| {
+// editor.set_leader_peer_id(leader_peer_id, cx)
+// })
+// }
+
+// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
+// Editor::should_unfollow_on_event(event, cx)
+// }
+
+// fn is_project_item(&self, _cx: &AppContext) -> bool {
+// false
+// }
+// }
+
+// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+
+// impl CollaborationHub for ChannelBufferCollaborationHub {
+// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+// self.0.read(cx).collaborators()
+// }
+
+// fn user_participant_indices<'a>(
+// &self,
+// cx: &'a AppContext,
+// ) -> &'a HashMap<u64, ParticipantIndex> {
+// self.0.read(cx).user_store().read(cx).participant_indices()
+// }
+// }
@@ -1,983 +1,983 @@
-use crate::{
- channel_view::ChannelView, is_channels_feature_enabled, render_avatar, 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,
- elements::*,
- platform::{CursorStyle, MouseButton},
- serde_json,
- views::{ItemType, Select, SelectStyle},
- AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
-};
-use language::LanguageRegistry;
-use menu::Confirm;
-use message_editor::MessageEditor;
-use project::Fs;
-use rich_text::RichText;
-use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::sync::Arc;
-use theme::{IconButton, Theme};
-use time::{OffsetDateTime, UtcOffset};
-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: ModelHandle<ChannelStore>,
- languages: Arc<LanguageRegistry>,
- active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
- message_list: ListState<ChatPanel>,
- input_editor: ViewHandle<MessageEditor>,
- channel_select: ViewHandle<Select>,
- local_timezone: UtcOffset,
- fs: Arc<dyn Fs>,
- width: Option<f32>,
- active: bool,
- pending_serialization: Task<Option<()>>,
- subscriptions: Vec<gpui::Subscription>,
- workspace: WeakViewHandle<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!(
- chat_panel,
- [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>) -> ViewHandle<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,
- Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
- 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 = &theme::current(cx).chat_panel.channel_select;
- SelectStyle {
- header: Default::default(),
- menu: style.menu,
- }
- })
- });
-
- let mut message_list =
- ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
- this.render_message(ix, cx)
- });
- message_list.set_scroll_handler(|visible_range, count, this, cx| {
- if visible_range.start < MESSAGE_LOADING_THRESHOLD {
- this.load_more_messages(&LoadMoreMessages, cx);
- }
- this.is_scrolled_to_bottom = visible_range.end == count;
- });
-
- cx.add_view(|cx| {
- let mut this = Self {
- fs,
- client,
- channel_store,
- languages,
- active_chat: Default::default(),
- pending_serialization: Task::ready(None),
- message_list,
- input_editor,
- channel_select,
- 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<ModelHandle<ChannelChat>> {
- self.active_chat.as_ref().map(|(chat, _)| chat.clone())
- }
-
- pub fn load(
- workspace: WeakViewHandle<Workspace>,
- cx: AsyncAppContext,
- ) -> Task<Result<ViewHandle<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: ModelHandle<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,
- _: ModelHandle<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<Self> {
- let theme = theme::current(cx);
- Flex::column()
- .with_child(
- ChildView::new(&self.channel_select, cx)
- .contained()
- .with_style(theme.chat_panel.channel_select.container),
- )
- .with_child(self.render_active_channel_messages(&theme))
- .with_child(self.render_input_box(&theme, cx))
- .into_any()
- }
-
- fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
- let messages = if self.active_chat.is_some() {
- List::new(self.message_list.clone())
- .contained()
- .with_style(theme.chat_panel.list)
- .into_any()
- } else {
- Empty::new().into_any()
- };
-
- messages.flex(1., true).into_any()
- }
-
- 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 theme = theme::current(cx);
- 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 style = if is_pending {
- &theme.chat_panel.pending_message
- } else if is_continuation {
- &theme.chat_panel.continuation_message
- } else {
- &theme.chat_panel.message
- };
-
- 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
- };
-
- enum MessageBackgroundHighlight {}
- MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
- let container = style.style_for(state);
- if is_continuation {
- Flex::row()
- .with_child(
- text.element(
- theme.editor.syntax.clone(),
- theme.chat_panel.rich_text.clone(),
- cx,
- )
- .flex(1., true),
- )
- .with_child(render_remove(message_id_to_remove, cx, &theme))
- .contained()
- .with_style(*container)
- .with_margin_bottom(if is_last {
- theme.chat_panel.last_message_bottom_spacing
- } else {
- 0.
- })
- .into_any()
- } else {
- Flex::column()
- .with_child(
- Flex::row()
- .with_child(
- Flex::row()
- .with_child(render_avatar(
- message.sender.avatar.clone(),
- &theme.chat_panel.avatar,
- theme.chat_panel.avatar_container,
- ))
- .with_child(
- Label::new(
- message.sender.github_login.clone(),
- theme.chat_panel.message_sender.text.clone(),
- )
- .contained()
- .with_style(theme.chat_panel.message_sender.container),
- )
- .with_child(
- Label::new(
- format_timestamp(
- message.timestamp,
- now,
- self.local_timezone,
- ),
- theme.chat_panel.message_timestamp.text.clone(),
- )
- .contained()
- .with_style(theme.chat_panel.message_timestamp.container),
- )
- .align_children_center()
- .flex(1., true),
- )
- .with_child(render_remove(message_id_to_remove, cx, &theme))
- .align_children_center(),
- )
- .with_child(
- Flex::row()
- .with_child(
- text.element(
- theme.editor.syntax.clone(),
- theme.chat_panel.rich_text.clone(),
- cx,
- )
- .flex(1., true),
- )
- // Add a spacer to make everything line up
- .with_child(render_remove(None, cx, &theme)),
- )
- .contained()
- .with_style(*container)
- .with_margin_bottom(if is_last {
- theme.chat_panel.last_message_bottom_spacing
- } else {
- 0.
- })
- .into_any()
- }
- })
- .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_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
- ChildView::new(&self.input_editor, cx)
- .contained()
- .with_style(theme.chat_panel.input_editor.container)
- .into_any()
- }
-
- fn render_channel_name(
- channel_store: &ModelHandle<ChannelStore>,
- ix: usize,
- item_type: ItemType,
- is_hovered: bool,
- workspace: WeakViewHandle<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,
- theme: &Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum SignInPromptLabel {}
-
- MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
- Label::new(
- "Sign in to use chat".to_string(),
- theme
- .chat_panel
- .sign_in_prompt
- .style_for(mouse_state)
- .clone(),
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, 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.message_list.scroll_to(ListOffset {
- item_ix,
- offset_in_item: 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>,
- theme: &Arc<Theme>,
-) -> AnyElement<ChatPanel> {
- 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 Entity for ChatPanel {
- type Event = Event;
-}
-
-impl View for ChatPanel {
- fn ui_name() -> &'static str {
- "ChatPanel"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = theme::current(cx);
- let element = if self.client.user_id().is_some() {
- self.render_channel(cx)
- } else {
- self.render_sign_in_prompt(&theme, cx)
- };
- element
- .contained()
- .with_style(theme.chat_panel.container)
- .constrained()
- .with_min_width(150.)
- .into_any()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- if matches!(
- *self.client.status().borrow(),
- client::Status::Connected { .. }
- ) {
- let editor = self.input_editor.read(cx).editor.clone();
- cx.focus(&editor);
- }
- }
-
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
- }
-}
-
-impl Panel for ChatPanel {
- fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- settings::get::<ChatPanelSettings>(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(|| settings::get::<ChatPanelSettings>(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 icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
- (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
- .then(|| "icons/conversations.svg")
- }
-
- fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
- ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
- }
-
- fn should_change_position_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::DockPositionChanged)
- }
-
- fn should_close_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::Dismissed)
- }
-
- fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
- self.has_focus
- }
-
- fn is_focus_event(event: &Self::Event) -> bool {
- matches!(event, Event::Focus)
- }
-}
-
-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<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)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::fonts::HighlightStyle;
- use pretty_assertions::assert_eq;
- use rich_text::{BackgroundKind, Highlight, RenderedRegion};
- use util::test::marked_text_ranges;
-
- #[gpui::test]
- fn test_render_markdown_with_mentions() {
- let language_registry = Arc::new(LanguageRegistry::test());
- let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
- let message = channel::ChannelMessage {
- id: ChannelMessageId::Saved(0),
- body,
- timestamp: OffsetDateTime::now_utc(),
- sender: Arc::new(client::User {
- github_login: "fgh".into(),
- avatar: None,
- id: 103,
- }),
- nonce: 5,
- mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
- };
-
- let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
-
- // Note that the "'" was replaced with ’ due to smart punctuation.
- let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
- assert_eq!(message.text, body);
- assert_eq!(
- message.highlights,
- vec![
- (
- ranges[0].clone(),
- HighlightStyle {
- italic: Some(true),
- ..Default::default()
- }
- .into()
- ),
- (ranges[1].clone(), Highlight::Mention),
- (
- ranges[2].clone(),
- HighlightStyle {
- weight: Some(gpui::fonts::Weight::BOLD),
- ..Default::default()
- }
- .into()
- ),
- (ranges[3].clone(), Highlight::SelfMention)
- ]
- );
- assert_eq!(
- message.regions,
- vec![
- RenderedRegion {
- background_kind: Some(BackgroundKind::Mention),
- link_url: None
- },
- RenderedRegion {
- background_kind: Some(BackgroundKind::SelfMention),
- link_url: None
- },
- ]
- );
- }
-}
+// use crate::{
+// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, 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,
+// elements::*,
+// platform::{CursorStyle, MouseButton},
+// serde_json,
+// views::{ItemType, Select, SelectStyle},
+// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
+// ViewContext, ViewHandle, WeakViewHandle,
+// };
+// use language::LanguageRegistry;
+// use menu::Confirm;
+// use message_editor::MessageEditor;
+// use project::Fs;
+// use rich_text::RichText;
+// use serde::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::sync::Arc;
+// use theme::{IconButton, Theme};
+// use time::{OffsetDateTime, UtcOffset};
+// 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: ModelHandle<ChannelStore>,
+// languages: Arc<LanguageRegistry>,
+// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
+// message_list: ListState<ChatPanel>,
+// input_editor: ViewHandle<MessageEditor>,
+// channel_select: ViewHandle<Select>,
+// local_timezone: UtcOffset,
+// fs: Arc<dyn Fs>,
+// width: Option<f32>,
+// active: bool,
+// pending_serialization: Task<Option<()>>,
+// subscriptions: Vec<gpui::Subscription>,
+// workspace: WeakViewHandle<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!(
+// chat_panel,
+// [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>) -> ViewHandle<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,
+// Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+// 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 = &theme::current(cx).chat_panel.channel_select;
+// SelectStyle {
+// header: Default::default(),
+// menu: style.menu,
+// }
+// })
+// });
+
+// let mut message_list =
+// ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
+// this.render_message(ix, cx)
+// });
+// message_list.set_scroll_handler(|visible_range, count, this, cx| {
+// if visible_range.start < MESSAGE_LOADING_THRESHOLD {
+// this.load_more_messages(&LoadMoreMessages, cx);
+// }
+// this.is_scrolled_to_bottom = visible_range.end == count;
+// });
+
+// cx.add_view(|cx| {
+// let mut this = Self {
+// fs,
+// client,
+// channel_store,
+// languages,
+// active_chat: Default::default(),
+// pending_serialization: Task::ready(None),
+// message_list,
+// input_editor,
+// channel_select,
+// 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<ModelHandle<ChannelChat>> {
+// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+// }
+
+// pub fn load(
+// workspace: WeakViewHandle<Workspace>,
+// cx: AsyncAppContext,
+// ) -> Task<Result<ViewHandle<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: ModelHandle<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,
+// _: ModelHandle<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<Self> {
+// let theme = theme::current(cx);
+// Flex::column()
+// .with_child(
+// ChildView::new(&self.channel_select, cx)
+// .contained()
+// .with_style(theme.chat_panel.channel_select.container),
+// )
+// .with_child(self.render_active_channel_messages(&theme))
+// .with_child(self.render_input_box(&theme, cx))
+// .into_any()
+// }
+
+// fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
+// let messages = if self.active_chat.is_some() {
+// List::new(self.message_list.clone())
+// .contained()
+// .with_style(theme.chat_panel.list)
+// .into_any()
+// } else {
+// Empty::new().into_any()
+// };
+
+// messages.flex(1., true).into_any()
+// }
+
+// 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 theme = theme::current(cx);
+// 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 style = if is_pending {
+// &theme.chat_panel.pending_message
+// } else if is_continuation {
+// &theme.chat_panel.continuation_message
+// } else {
+// &theme.chat_panel.message
+// };
+
+// 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
+// };
+
+// enum MessageBackgroundHighlight {}
+// MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
+// let container = style.style_for(state);
+// if is_continuation {
+// Flex::row()
+// .with_child(
+// text.element(
+// theme.editor.syntax.clone(),
+// theme.chat_panel.rich_text.clone(),
+// cx,
+// )
+// .flex(1., true),
+// )
+// .with_child(render_remove(message_id_to_remove, cx, &theme))
+// .contained()
+// .with_style(*container)
+// .with_margin_bottom(if is_last {
+// theme.chat_panel.last_message_bottom_spacing
+// } else {
+// 0.
+// })
+// .into_any()
+// } else {
+// Flex::column()
+// .with_child(
+// Flex::row()
+// .with_child(
+// Flex::row()
+// .with_child(render_avatar(
+// message.sender.avatar.clone(),
+// &theme.chat_panel.avatar,
+// theme.chat_panel.avatar_container,
+// ))
+// .with_child(
+// Label::new(
+// message.sender.github_login.clone(),
+// theme.chat_panel.message_sender.text.clone(),
+// )
+// .contained()
+// .with_style(theme.chat_panel.message_sender.container),
+// )
+// .with_child(
+// Label::new(
+// format_timestamp(
+// message.timestamp,
+// now,
+// self.local_timezone,
+// ),
+// theme.chat_panel.message_timestamp.text.clone(),
+// )
+// .contained()
+// .with_style(theme.chat_panel.message_timestamp.container),
+// )
+// .align_children_center()
+// .flex(1., true),
+// )
+// .with_child(render_remove(message_id_to_remove, cx, &theme))
+// .align_children_center(),
+// )
+// .with_child(
+// Flex::row()
+// .with_child(
+// text.element(
+// theme.editor.syntax.clone(),
+// theme.chat_panel.rich_text.clone(),
+// cx,
+// )
+// .flex(1., true),
+// )
+// // Add a spacer to make everything line up
+// .with_child(render_remove(None, cx, &theme)),
+// )
+// .contained()
+// .with_style(*container)
+// .with_margin_bottom(if is_last {
+// theme.chat_panel.last_message_bottom_spacing
+// } else {
+// 0.
+// })
+// .into_any()
+// }
+// })
+// .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_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
+// ChildView::new(&self.input_editor, cx)
+// .contained()
+// .with_style(theme.chat_panel.input_editor.container)
+// .into_any()
+// }
+
+// fn render_channel_name(
+// channel_store: &ModelHandle<ChannelStore>,
+// ix: usize,
+// item_type: ItemType,
+// is_hovered: bool,
+// workspace: WeakViewHandle<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,
+// theme: &Arc<Theme>,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum SignInPromptLabel {}
+
+// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+// Label::new(
+// "Sign in to use chat".to_string(),
+// theme
+// .chat_panel
+// .sign_in_prompt
+// .style_for(mouse_state)
+// .clone(),
+// )
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, 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.message_list.scroll_to(ListOffset {
+// item_ix,
+// offset_in_item: 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>,
+// theme: &Arc<Theme>,
+// ) -> AnyElement<ChatPanel> {
+// 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 Entity for ChatPanel {
+// type Event = Event;
+// }
+
+// impl View for ChatPanel {
+// fn ui_name() -> &'static str {
+// "ChatPanel"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// let theme = theme::current(cx);
+// let element = if self.client.user_id().is_some() {
+// self.render_channel(cx)
+// } else {
+// self.render_sign_in_prompt(&theme, cx)
+// };
+// element
+// .contained()
+// .with_style(theme.chat_panel.container)
+// .constrained()
+// .with_min_width(150.)
+// .into_any()
+// }
+
+// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+// self.has_focus = true;
+// if matches!(
+// *self.client.status().borrow(),
+// client::Status::Connected { .. }
+// ) {
+// let editor = self.input_editor.read(cx).editor.clone();
+// cx.focus(&editor);
+// }
+// }
+
+// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+// self.has_focus = false;
+// }
+// }
+
+// impl Panel for ChatPanel {
+// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+// settings::get::<ChatPanelSettings>(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(|| settings::get::<ChatPanelSettings>(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 icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+// (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
+// .then(|| "icons/conversations.svg")
+// }
+
+// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
+// }
+
+// fn should_change_position_on_event(event: &Self::Event) -> bool {
+// matches!(event, Event::DockPositionChanged)
+// }
+
+// fn should_close_on_event(event: &Self::Event) -> bool {
+// matches!(event, Event::Dismissed)
+// }
+
+// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+// self.has_focus
+// }
+
+// fn is_focus_event(event: &Self::Event) -> bool {
+// matches!(event, Event::Focus)
+// }
+// }
+
+// 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<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)
+// }
+
+// #[cfg(test)]
+// mod tests {
+// use super::*;
+// use gpui::fonts::HighlightStyle;
+// use pretty_assertions::assert_eq;
+// use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+// use util::test::marked_text_ranges;
+
+// #[gpui::test]
+// fn test_render_markdown_with_mentions() {
+// let language_registry = Arc::new(LanguageRegistry::test());
+// let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
+// let message = channel::ChannelMessage {
+// id: ChannelMessageId::Saved(0),
+// body,
+// timestamp: OffsetDateTime::now_utc(),
+// sender: Arc::new(client::User {
+// github_login: "fgh".into(),
+// avatar: None,
+// id: 103,
+// }),
+// nonce: 5,
+// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
+// };
+
+// let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
+
+// // Note that the "'" was replaced with ’ due to smart punctuation.
+// let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
+// assert_eq!(message.text, body);
+// assert_eq!(
+// message.highlights,
+// vec![
+// (
+// ranges[0].clone(),
+// HighlightStyle {
+// italic: Some(true),
+// ..Default::default()
+// }
+// .into()
+// ),
+// (ranges[1].clone(), Highlight::Mention),
+// (
+// ranges[2].clone(),
+// HighlightStyle {
+// weight: Some(gpui::fonts::Weight::BOLD),
+// ..Default::default()
+// }
+// .into()
+// ),
+// (ranges[3].clone(), Highlight::SelfMention)
+// ]
+// );
+// assert_eq!(
+// message.regions,
+// vec![
+// RenderedRegion {
+// background_kind: Some(BackgroundKind::Mention),
+// link_url: None
+// },
+// RenderedRegion {
+// background_kind: Some(BackgroundKind::SelfMention),
+// link_url: None
+// },
+// ]
+// );
+// }
+// }
@@ -1,3548 +1,3548 @@
-mod channel_modal;
-mod contact_finder;
-
-use crate::{
- channel_view::{self, ChannelView},
- chat_panel::ChatPanel,
- face_pile::FacePile,
- panel_settings, CollaborationPanelSettings,
-};
-use anyhow::Result;
-use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use channel_modal::ChannelModal;
-use client::{
- proto::{self, PeerId},
- Client, Contact, User, UserStore,
-};
-use contact_finder::ContactFinder;
-use context_menu::{ContextMenu, ContextMenuItem};
-use db::kvp::KEY_VALUE_STORE;
-use drag_and_drop::{DragAndDrop, Draggable};
-use editor::{Cancel, Editor};
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use futures::StreamExt;
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
- actions,
- elements::{
- Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
- ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
- SafeStylable, Stack, Svg,
- },
- fonts::TextStyle,
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- impl_actions,
- platform::{CursorStyle, MouseButton, PromptLevel},
- serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
- ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
-};
-use menu::{Confirm, SelectNext, SelectPrev};
-use project::{Fs, Project};
-use serde_derive::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
-use theme::{components::ComponentExt, IconButton, Interactive};
-use util::{maybe, ResultExt, TryFutureExt};
-use workspace::{
- dock::{DockPosition, Panel},
- item::ItemHandle,
- FollowNextCollaborator, Workspace,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleCollapse {
- location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct NewChannel {
- location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RenameChannel {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleSelectedIx {
- ix: usize,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RemoveChannel {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct InviteMembers {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ManageMembers {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct OpenChannelNotes {
- pub channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelCall {
- pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelChat {
- pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct CopyChannelLink {
- pub channel_id: u64,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct StartMoveChannelFor {
- channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct MoveChannel {
- to: ChannelId,
-}
-
-actions!(
- collab_panel,
- [
- ToggleFocus,
- Remove,
- Secondary,
- CollapseSelectedChannel,
- ExpandSelectedChannel,
- StartMoveChannel,
- MoveSelected,
- InsertSpace,
- ]
-);
-
-impl_actions!(
- collab_panel,
- [
- RemoveChannel,
- NewChannel,
- InviteMembers,
- ManageMembers,
- RenameChannel,
- ToggleCollapse,
- OpenChannelNotes,
- JoinChannelCall,
- JoinChannelChat,
- CopyChannelLink,
- StartMoveChannelFor,
- MoveChannel,
- ToggleSelectedIx
- ]
-);
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-struct ChannelMoveClipboard {
- channel_id: ChannelId,
-}
-
-const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
-
-pub fn init(cx: &mut AppContext) {
- settings::register::<panel_settings::CollaborationPanelSettings>(cx);
- contact_finder::init(cx);
- channel_modal::init(cx);
- channel_view::init(cx);
-
- cx.add_action(CollabPanel::cancel);
- cx.add_action(CollabPanel::select_next);
- cx.add_action(CollabPanel::select_prev);
- cx.add_action(CollabPanel::confirm);
- cx.add_action(CollabPanel::insert_space);
- cx.add_action(CollabPanel::remove);
- cx.add_action(CollabPanel::remove_selected_channel);
- cx.add_action(CollabPanel::show_inline_context_menu);
- cx.add_action(CollabPanel::new_subchannel);
- cx.add_action(CollabPanel::invite_members);
- cx.add_action(CollabPanel::manage_members);
- cx.add_action(CollabPanel::rename_selected_channel);
- cx.add_action(CollabPanel::rename_channel);
- cx.add_action(CollabPanel::toggle_channel_collapsed_action);
- cx.add_action(CollabPanel::collapse_selected_channel);
- cx.add_action(CollabPanel::expand_selected_channel);
- cx.add_action(CollabPanel::open_channel_notes);
- cx.add_action(CollabPanel::join_channel_chat);
- cx.add_action(CollabPanel::copy_channel_link);
-
- cx.add_action(
- |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
- if panel.selection.take() != Some(action.ix) {
- panel.selection = Some(action.ix)
- }
-
- cx.notify();
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel,
- action: &StartMoveChannelFor,
- _: &mut ViewContext<CollabPanel>| {
- panel.channel_clipboard = Some(ChannelMoveClipboard {
- channel_id: action.channel_id,
- });
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
- if let Some(channel) = panel.selected_channel() {
- panel.channel_clipboard = Some(ChannelMoveClipboard {
- channel_id: channel.id,
- })
- }
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
- let Some(clipboard) = panel.channel_clipboard.take() else {
- return;
- };
- let Some(selected_channel) = panel.selected_channel() else {
- return;
- };
-
- panel
- .channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
- })
- .detach_and_log_err(cx)
- },
- );
-
- cx.add_action(
- |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
- if let Some(clipboard) = panel.channel_clipboard.take() {
- panel.channel_store.update(cx, |channel_store, cx| {
- channel_store
- .move_channel(clipboard.channel_id, Some(action.to), cx)
- .detach_and_log_err(cx)
- })
- }
- },
- );
-}
-
-#[derive(Debug)]
-pub enum ChannelEditingState {
- Create {
- location: Option<ChannelId>,
- pending_name: Option<String>,
- },
- Rename {
- location: ChannelId,
- pending_name: Option<String>,
- },
-}
-
-impl ChannelEditingState {
- fn pending_name(&self) -> Option<&str> {
- match self {
- ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
- ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
- }
- }
-}
-
-pub struct CollabPanel {
- width: Option<f32>,
- fs: Arc<dyn Fs>,
- has_focus: bool,
- channel_clipboard: Option<ChannelMoveClipboard>,
- pending_serialization: Task<Option<()>>,
- context_menu: ViewHandle<ContextMenu>,
- filter_editor: ViewHandle<Editor>,
- channel_name_editor: ViewHandle<Editor>,
- channel_editing_state: Option<ChannelEditingState>,
- entries: Vec<ListEntry>,
- selection: Option<usize>,
- user_store: ModelHandle<UserStore>,
- client: Arc<Client>,
- channel_store: ModelHandle<ChannelStore>,
- project: ModelHandle<Project>,
- match_candidates: Vec<StringMatchCandidate>,
- list_state: ListState<Self>,
- subscriptions: Vec<Subscription>,
- collapsed_sections: Vec<Section>,
- collapsed_channels: Vec<ChannelId>,
- drag_target_channel: ChannelDragTarget,
- workspace: WeakViewHandle<Workspace>,
- context_menu_on_selected: bool,
-}
-
-#[derive(PartialEq, Eq)]
-enum ChannelDragTarget {
- None,
- Root,
- Channel(ChannelId),
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedCollabPanel {
- width: Option<f32>,
- collapsed_channels: Option<Vec<ChannelId>>,
-}
-
-#[derive(Debug)]
-pub enum Event {
- DockPositionChanged,
- Focus,
- Dismissed,
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-enum Section {
- ActiveCall,
- Channels,
- ChannelInvites,
- ContactRequests,
- Contacts,
- Online,
- Offline,
-}
-
-#[derive(Clone, Debug)]
-enum ListEntry {
- Header(Section),
- CallParticipant {
- user: Arc<User>,
- peer_id: Option<PeerId>,
- is_pending: bool,
- },
- ParticipantProject {
- project_id: u64,
- worktree_root_names: Vec<String>,
- host_user_id: u64,
- is_last: bool,
- },
- ParticipantScreen {
- peer_id: Option<PeerId>,
- is_last: bool,
- },
- IncomingRequest(Arc<User>),
- OutgoingRequest(Arc<User>),
- ChannelInvite(Arc<Channel>),
- Channel {
- channel: Arc<Channel>,
- depth: usize,
- has_children: bool,
- },
- ChannelNotes {
- channel_id: ChannelId,
- },
- ChannelChat {
- channel_id: ChannelId,
- },
- ChannelEditor {
- depth: usize,
- },
- Contact {
- contact: Arc<Contact>,
- calling: bool,
- },
- ContactPlaceholder,
-}
-
-impl Entity for CollabPanel {
- type Event = Event;
-}
-
-impl CollabPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
- cx.add_view::<Self, _>(|cx| {
- let view_id = cx.view_id();
-
- let filter_editor = cx.add_view(|cx| {
- let mut editor = Editor::single_line(
- Some(Arc::new(|theme| {
- theme.collab_panel.user_query_editor.clone()
- })),
- cx,
- );
- editor.set_placeholder_text("Filter channels, contacts", cx);
- editor
- });
-
- cx.subscribe(&filter_editor, |this, _, event, cx| {
- if let editor::Event::BufferEdited = event {
- let query = this.filter_editor.read(cx).text(cx);
- if !query.is_empty() {
- this.selection.take();
- }
- this.update_entries(true, cx);
- if !query.is_empty() {
- this.selection = this
- .entries
- .iter()
- .position(|entry| !matches!(entry, ListEntry::Header(_)));
- }
- } else if let editor::Event::Blurred = event {
- let query = this.filter_editor.read(cx).text(cx);
- if query.is_empty() {
- this.selection.take();
- this.update_entries(true, cx);
- }
- }
- })
- .detach();
-
- let channel_name_editor = cx.add_view(|cx| {
- Editor::single_line(
- Some(Arc::new(|theme| {
- theme.collab_panel.user_query_editor.clone()
- })),
- cx,
- )
- });
-
- cx.subscribe(&channel_name_editor, |this, _, event, cx| {
- if let editor::Event::Blurred = event {
- if let Some(state) = &this.channel_editing_state {
- if state.pending_name().is_some() {
- return;
- }
- }
- this.take_editing_state(cx);
- this.update_entries(false, cx);
- cx.notify();
- }
- })
- .detach();
-
- let list_state =
- ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
- let theme = theme::current(cx).clone();
- let is_selected = this.selection == Some(ix);
- let current_project_id = this.project.read(cx).remote_id();
-
- match &this.entries[ix] {
- ListEntry::Header(section) => {
- let is_collapsed = this.collapsed_sections.contains(section);
- this.render_header(*section, &theme, is_selected, is_collapsed, cx)
- }
- ListEntry::CallParticipant {
- user,
- peer_id,
- is_pending,
- } => Self::render_call_participant(
- user,
- *peer_id,
- this.user_store.clone(),
- *is_pending,
- is_selected,
- &theme,
- cx,
- ),
- ListEntry::ParticipantProject {
- project_id,
- worktree_root_names,
- host_user_id,
- is_last,
- } => Self::render_participant_project(
- *project_id,
- worktree_root_names,
- *host_user_id,
- Some(*project_id) == current_project_id,
- *is_last,
- is_selected,
- &theme,
- cx,
- ),
- ListEntry::ParticipantScreen { peer_id, is_last } => {
- Self::render_participant_screen(
- *peer_id,
- *is_last,
- is_selected,
- &theme.collab_panel,
- cx,
- )
- }
- ListEntry::Channel {
- channel,
- depth,
- has_children,
- } => {
- let channel_row = this.render_channel(
- &*channel,
- *depth,
- &theme,
- is_selected,
- *has_children,
- ix,
- cx,
- );
-
- if is_selected && this.context_menu_on_selected {
- Stack::new()
- .with_child(channel_row)
- .with_child(
- ChildView::new(&this.context_menu, cx)
- .aligned()
- .bottom()
- .right(),
- )
- .into_any()
- } else {
- return channel_row;
- }
- }
- ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
- *channel_id,
- &theme.collab_panel,
- is_selected,
- ix,
- cx,
- ),
- ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
- *channel_id,
- &theme.collab_panel,
- is_selected,
- ix,
- cx,
- ),
- ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
- channel.clone(),
- this.channel_store.clone(),
- &theme.collab_panel,
- is_selected,
- cx,
- ),
- ListEntry::IncomingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- &theme.collab_panel,
- true,
- is_selected,
- cx,
- ),
- ListEntry::OutgoingRequest(user) => Self::render_contact_request(
- user.clone(),
- this.user_store.clone(),
- &theme.collab_panel,
- false,
- is_selected,
- cx,
- ),
- ListEntry::Contact { contact, calling } => Self::render_contact(
- contact,
- *calling,
- &this.project,
- &theme,
- is_selected,
- cx,
- ),
- ListEntry::ChannelEditor { depth } => {
- this.render_channel_editor(&theme, *depth, cx)
- }
- ListEntry::ContactPlaceholder => {
- this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
- }
- }
- });
-
- let mut this = Self {
- width: None,
- has_focus: false,
- channel_clipboard: None,
- fs: workspace.app_state().fs.clone(),
- pending_serialization: Task::ready(None),
- context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
- channel_name_editor,
- filter_editor,
- entries: Vec::default(),
- channel_editing_state: None,
- selection: None,
- user_store: workspace.user_store().clone(),
- channel_store: ChannelStore::global(cx),
- project: workspace.project().clone(),
- subscriptions: Vec::default(),
- match_candidates: Vec::default(),
- collapsed_sections: vec![Section::Offline],
- collapsed_channels: Vec::default(),
- workspace: workspace.weak_handle(),
- client: workspace.app_state().client.clone(),
- context_menu_on_selected: true,
- drag_target_channel: ChannelDragTarget::None,
- list_state,
- };
-
- this.update_entries(false, cx);
-
- // Update the dock position when the setting changes.
- 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();
- }),
- );
-
- let active_call = ActiveCall::global(cx);
- this.subscriptions
- .push(cx.observe(&this.user_store, |this, _, cx| {
- this.update_entries(true, cx)
- }));
- this.subscriptions
- .push(cx.observe(&this.channel_store, |this, _, cx| {
- this.update_entries(true, cx)
- }));
- this.subscriptions
- .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
- this.subscriptions
- .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
- this.update_entries(true, cx)
- }));
- this.subscriptions.push(cx.subscribe(
- &this.channel_store,
- |this, _channel_store, e, cx| match e {
- ChannelEvent::ChannelCreated(channel_id)
- | ChannelEvent::ChannelRenamed(channel_id) => {
- if this.take_editing_state(cx) {
- this.update_entries(false, cx);
- this.selection = this.entries.iter().position(|entry| {
- if let ListEntry::Channel { channel, .. } = entry {
- channel.id == *channel_id
- } else {
- false
- }
- });
- }
- }
- },
- ));
-
- this
- })
- }
-
- pub fn load(
- workspace: WeakViewHandle<Workspace>,
- cx: AsyncAppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
- cx.spawn(|mut cx| async move {
- let serialized_panel = if let Some(panel) = cx
- .background()
- .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
- .await
- .log_err()
- .flatten()
- {
- match serde_json::from_str::<SerializedCollabPanel>(&panel) {
- Ok(panel) => Some(panel),
- Err(err) => {
- log::error!("Failed to deserialize collaboration panel: {}", err);
- None
- }
- }
- } else {
- None
- };
-
- workspace.update(&mut cx, |workspace, cx| {
- let panel = CollabPanel::new(workspace, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width;
- panel.collapsed_channels = serialized_panel
- .collapsed_channels
- .unwrap_or_else(|| Vec::new());
- cx.notify();
- });
- }
- panel
- })
- })
- }
-
- fn serialize(&mut self, cx: &mut ViewContext<Self>) {
- let width = self.width;
- let collapsed_channels = self.collapsed_channels.clone();
- self.pending_serialization = cx.background().spawn(
- async move {
- KEY_VALUE_STORE
- .write_kvp(
- COLLABORATION_PANEL_KEY.into(),
- serde_json::to_string(&SerializedCollabPanel {
- width,
- collapsed_channels: Some(collapsed_channels),
- })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
- fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.read(cx);
- let user_store = self.user_store.read(cx);
- let query = self.filter_editor.read(cx).text(cx);
- let executor = cx.background().clone();
-
- let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
- let old_entries = mem::take(&mut self.entries);
- let mut scroll_to_top = false;
-
- if let Some(room) = ActiveCall::global(cx).read(cx).room() {
- self.entries.push(ListEntry::Header(Section::ActiveCall));
- if !old_entries
- .iter()
- .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
- {
- scroll_to_top = true;
- }
-
- if !self.collapsed_sections.contains(&Section::ActiveCall) {
- let room = room.read(cx);
-
- if let Some(channel_id) = room.channel_id() {
- self.entries.push(ListEntry::ChannelNotes { channel_id });
- self.entries.push(ListEntry::ChannelChat { channel_id })
- }
-
- // Populate the active user.
- if let Some(user) = user_store.current_user() {
- self.match_candidates.clear();
- self.match_candidates.push(StringMatchCandidate {
- id: 0,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- });
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- if !matches.is_empty() {
- let user_id = user.id;
- self.entries.push(ListEntry::CallParticipant {
- user,
- peer_id: None,
- is_pending: false,
- });
- let mut projects = room.local_participant().projects.iter().peekable();
- while let Some(project) = projects.next() {
- self.entries.push(ListEntry::ParticipantProject {
- project_id: project.id,
- worktree_root_names: project.worktree_root_names.clone(),
- host_user_id: user_id,
- is_last: projects.peek().is_none() && !room.is_screen_sharing(),
- });
- }
- if room.is_screen_sharing() {
- self.entries.push(ListEntry::ParticipantScreen {
- peer_id: None,
- is_last: true,
- });
- }
- }
- }
-
- // Populate remote participants.
- self.match_candidates.clear();
- self.match_candidates
- .extend(room.remote_participants().iter().map(|(_, participant)| {
- StringMatchCandidate {
- id: participant.user.id as usize,
- string: participant.user.github_login.clone(),
- char_bag: participant.user.github_login.chars().collect(),
- }
- }));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- for mat in matches {
- let user_id = mat.candidate_id as u64;
- let participant = &room.remote_participants()[&user_id];
- self.entries.push(ListEntry::CallParticipant {
- user: participant.user.clone(),
- peer_id: Some(participant.peer_id),
- is_pending: false,
- });
- let mut projects = participant.projects.iter().peekable();
- while let Some(project) = projects.next() {
- self.entries.push(ListEntry::ParticipantProject {
- project_id: project.id,
- worktree_root_names: project.worktree_root_names.clone(),
- host_user_id: participant.user.id,
- is_last: projects.peek().is_none()
- && participant.video_tracks.is_empty(),
- });
- }
- if !participant.video_tracks.is_empty() {
- self.entries.push(ListEntry::ParticipantScreen {
- peer_id: Some(participant.peer_id),
- is_last: true,
- });
- }
- }
-
- // Populate pending participants.
- self.match_candidates.clear();
- self.match_candidates
- .extend(room.pending_participants().iter().enumerate().map(
- |(id, participant)| StringMatchCandidate {
- id,
- string: participant.github_login.clone(),
- char_bag: participant.github_login.chars().collect(),
- },
- ));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- self.entries
- .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
- user: room.pending_participants()[mat.candidate_id].clone(),
- peer_id: None,
- is_pending: true,
- }));
- }
- }
-
- let mut request_entries = Vec::new();
-
- if cx.has_flag::<ChannelsAlpha>() {
- self.entries.push(ListEntry::Header(Section::Channels));
-
- if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(channel_store.ordered_channels().enumerate().map(
- |(ix, (_, channel))| StringMatchCandidate {
- id: ix,
- string: channel.name.clone(),
- char_bag: channel.name.chars().collect(),
- },
- ));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- if let Some(state) = &self.channel_editing_state {
- if matches!(state, ChannelEditingState::Create { location: None, .. }) {
- self.entries.push(ListEntry::ChannelEditor { depth: 0 });
- }
- }
- let mut collapse_depth = None;
- for mat in matches {
- let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
- let depth = channel.parent_path.len();
-
- if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
- collapse_depth = Some(depth);
- } else if let Some(collapsed_depth) = collapse_depth {
- if depth > collapsed_depth {
- continue;
- }
- if self.is_channel_collapsed(channel.id) {
- collapse_depth = Some(depth);
- } else {
- collapse_depth = None;
- }
- }
-
- let has_children = channel_store
- .channel_at_index(mat.candidate_id + 1)
- .map_or(false, |next_channel| {
- next_channel.parent_path.ends_with(&[channel.id])
- });
-
- match &self.channel_editing_state {
- Some(ChannelEditingState::Create {
- location: parent_id,
- ..
- }) if *parent_id == Some(channel.id) => {
- self.entries.push(ListEntry::Channel {
- channel: channel.clone(),
- depth,
- has_children: false,
- });
- self.entries
- .push(ListEntry::ChannelEditor { depth: depth + 1 });
- }
- Some(ChannelEditingState::Rename {
- location: parent_id,
- ..
- }) if parent_id == &channel.id => {
- self.entries.push(ListEntry::ChannelEditor { depth });
- }
- _ => {
- self.entries.push(ListEntry::Channel {
- channel: channel.clone(),
- depth,
- has_children,
- });
- }
- }
- }
- }
-
- let channel_invites = channel_store.channel_invitations();
- if !channel_invites.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
- StringMatchCandidate {
- id: ix,
- string: channel.name.clone(),
- char_bag: channel.name.chars().collect(),
- }
- }));
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(matches.iter().map(|mat| {
- ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
- }));
-
- if !request_entries.is_empty() {
- self.entries
- .push(ListEntry::Header(Section::ChannelInvites));
- if !self.collapsed_sections.contains(&Section::ChannelInvites) {
- self.entries.append(&mut request_entries);
- }
- }
- }
- }
-
- self.entries.push(ListEntry::Header(Section::Contacts));
-
- request_entries.clear();
- let incoming = user_store.incoming_contact_requests();
- if !incoming.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- incoming
- .iter()
- .enumerate()
- .map(|(ix, user)| StringMatchCandidate {
- id: ix,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- }),
- );
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(
- matches
- .iter()
- .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
- );
- }
-
- let outgoing = user_store.outgoing_contact_requests();
- if !outgoing.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- outgoing
- .iter()
- .enumerate()
- .map(|(ix, user)| StringMatchCandidate {
- id: ix,
- string: user.github_login.clone(),
- char_bag: user.github_login.chars().collect(),
- }),
- );
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
- request_entries.extend(
- matches
- .iter()
- .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
- );
- }
-
- if !request_entries.is_empty() {
- self.entries
- .push(ListEntry::Header(Section::ContactRequests));
- if !self.collapsed_sections.contains(&Section::ContactRequests) {
- self.entries.append(&mut request_entries);
- }
- }
-
- let contacts = user_store.contacts();
- if !contacts.is_empty() {
- self.match_candidates.clear();
- self.match_candidates
- .extend(
- contacts
- .iter()
- .enumerate()
- .map(|(ix, contact)| StringMatchCandidate {
- id: ix,
- string: contact.user.github_login.clone(),
- char_bag: contact.user.github_login.chars().collect(),
- }),
- );
-
- let matches = executor.block(match_strings(
- &self.match_candidates,
- &query,
- true,
- usize::MAX,
- &Default::default(),
- executor.clone(),
- ));
-
- let (online_contacts, offline_contacts) = matches
- .iter()
- .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
- for (matches, section) in [
- (online_contacts, Section::Online),
- (offline_contacts, Section::Offline),
- ] {
- if !matches.is_empty() {
- self.entries.push(ListEntry::Header(section));
- if !self.collapsed_sections.contains(§ion) {
- let active_call = &ActiveCall::global(cx).read(cx);
- for mat in matches {
- let contact = &contacts[mat.candidate_id];
- self.entries.push(ListEntry::Contact {
- contact: contact.clone(),
- calling: active_call.pending_invites().contains(&contact.user.id),
- });
- }
- }
- }
- }
- }
-
- if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
- self.entries.push(ListEntry::ContactPlaceholder);
- }
-
- if select_same_item {
- if let Some(prev_selected_entry) = prev_selected_entry {
- self.selection.take();
- for (ix, entry) in self.entries.iter().enumerate() {
- if *entry == prev_selected_entry {
- self.selection = Some(ix);
- break;
- }
- }
- }
- } else {
- self.selection = self.selection.and_then(|prev_selection| {
- if self.entries.is_empty() {
- None
- } else {
- Some(prev_selection.min(self.entries.len() - 1))
- }
- });
- }
-
- let old_scroll_top = self.list_state.logical_scroll_top();
-
- self.list_state.reset(self.entries.len());
-
- if scroll_to_top {
- self.list_state.scroll_to(ListOffset::default());
- } else {
- // Attempt to maintain the same scroll position.
- if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
- let new_scroll_top = self
- .entries
- .iter()
- .position(|entry| entry == old_top_entry)
- .map(|item_ix| ListOffset {
- item_ix,
- offset_in_item: old_scroll_top.offset_in_item,
- })
- .or_else(|| {
- let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
- let item_ix = self
- .entries
- .iter()
- .position(|entry| entry == entry_after_old_top)?;
- Some(ListOffset {
- item_ix,
- offset_in_item: 0.,
- })
- })
- .or_else(|| {
- let entry_before_old_top =
- old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
- let item_ix = self
- .entries
- .iter()
- .position(|entry| entry == entry_before_old_top)?;
- Some(ListOffset {
- item_ix,
- offset_in_item: 0.,
- })
- });
-
- self.list_state
- .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
- }
- }
-
- cx.notify();
- }
-
- fn render_call_participant(
- user: &User,
- peer_id: Option<PeerId>,
- user_store: ModelHandle<UserStore>,
- is_pending: bool,
- is_selected: bool,
- theme: &theme::Theme,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum CallParticipant {}
- enum CallParticipantTooltip {}
- enum LeaveCallButton {}
- enum LeaveCallTooltip {}
-
- let collab_theme = &theme.collab_panel;
-
- let is_current_user =
- user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-
- let content = MouseEventHandler::new::<CallParticipant, _>(
- user.id as usize,
- cx,
- |mouse_state, cx| {
- let style = if is_current_user {
- *collab_theme
- .contact_row
- .in_state(is_selected)
- .style_for(&mut Default::default())
- } else {
- *collab_theme
- .contact_row
- .in_state(is_selected)
- .style_for(mouse_state)
- };
-
- Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(collab_theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(
- user.github_login.clone(),
- collab_theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(collab_theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .with_children(if is_pending {
- Some(
- Label::new("Calling", collab_theme.calling_indicator.text.clone())
- .contained()
- .with_style(collab_theme.calling_indicator.container)
- .aligned()
- .into_any(),
- )
- } else if is_current_user {
- Some(
- MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
- render_icon_button(
- theme
- .collab_panel
- .leave_call_button
- .style_for(is_selected, state),
- "icons/exit.svg",
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, _, cx| {
- Self::leave_call(cx);
- })
- .with_tooltip::<LeaveCallTooltip>(
- 0,
- "Leave call",
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any(),
- )
- } else {
- None
- })
- .constrained()
- .with_height(collab_theme.row_height)
- .contained()
- .with_style(style)
- },
- );
-
- if is_current_user || is_pending || peer_id.is_none() {
- return content.into_any();
- }
-
- let tooltip = format!("Follow {}", user.github_login);
-
- content
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace
- .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
- .map(|task| task.detach_and_log_err(cx));
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .with_tooltip::<CallParticipantTooltip>(
- user.id as usize,
- tooltip,
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- }
-
- fn render_participant_project(
- project_id: u64,
- worktree_root_names: &[String],
- host_user_id: u64,
- is_current: bool,
- is_last: bool,
- is_selected: bool,
- theme: &theme::Theme,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum JoinProject {}
- enum JoinProjectTooltip {}
-
- let collab_theme = &theme.collab_panel;
- let host_avatar_width = collab_theme
- .contact_avatar
- .width
- .or(collab_theme.contact_avatar.height)
- .unwrap_or(0.);
- let tree_branch = collab_theme.tree_branch;
- let project_name = if worktree_root_names.is_empty() {
- "untitled".to_string()
- } else {
- worktree_root_names.join(", ")
- };
-
- let content =
- MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
- let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- let row = if is_current {
- collab_theme
- .project_row
- .in_state(true)
- .style_for(&mut Default::default())
- } else {
- collab_theme
- .project_row
- .in_state(is_selected)
- .style_for(mouse_state)
- };
-
- Flex::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- is_last,
- vec2f(host_avatar_width, collab_theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/file_icons/folder.svg")
- .with_color(collab_theme.channel_hash.color)
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new(project_name.clone(), row.name.text.clone())
- .aligned()
- .left()
- .contained()
- .with_style(row.name.container)
- .flex(1., false),
- )
- .constrained()
- .with_height(collab_theme.row_height)
- .contained()
- .with_style(row.container)
- });
-
- if is_current {
- return content.into_any();
- }
-
- content
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_remote_project(project_id, host_user_id, app_state, cx)
- .detach_and_log_err(cx);
- }
- })
- .with_tooltip::<JoinProjectTooltip>(
- project_id as usize,
- format!("Open {}", project_name),
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- }
-
- fn render_participant_screen(
- peer_id: Option<PeerId>,
- is_last: bool,
- is_selected: bool,
- theme: &theme::CollabPanel,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum OpenSharedScreen {}
-
- let host_avatar_width = theme
- .contact_avatar
- .width
- .or(theme.contact_avatar.height)
- .unwrap_or(0.);
- let tree_branch = theme.tree_branch;
-
- let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
- peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
- cx,
- |mouse_state, cx| {
- let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- let row = theme
- .project_row
- .in_state(is_selected)
- .style_for(mouse_state);
-
- Flex::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- is_last,
- vec2f(host_avatar_width, theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/desktop.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new("Screen", row.name.text.clone())
- .aligned()
- .left()
- .contained()
- .with_style(row.name.container)
- .flex(1., false),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(row.container)
- },
- );
- if peer_id.is_none() {
- return handler.into_any();
- }
- handler
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace.open_shared_screen(peer_id.unwrap(), cx)
- });
- }
- })
- .into_any()
- }
-
- fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(_) = self.channel_editing_state.take() {
- self.channel_name_editor.update(cx, |editor, cx| {
- editor.set_text("", cx);
- });
- true
- } else {
- false
- }
- }
-
- fn render_header(
- &self,
- section: Section,
- theme: &theme::Theme,
- is_selected: bool,
- is_collapsed: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum Header {}
- enum LeaveCallContactList {}
- enum AddChannel {}
-
- let tooltip_style = &theme.tooltip;
- let mut channel_link = None;
- let mut channel_tooltip_text = None;
- let mut channel_icon = None;
- let mut is_dragged_over = false;
-
- let text = match section {
- Section::ActiveCall => {
- let channel_name = maybe!({
- let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
-
- let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
-
- channel_link = Some(channel.link());
- (channel_icon, channel_tooltip_text) = match channel.visibility {
- proto::ChannelVisibility::Public => {
- (Some("icons/public.svg"), Some("Copy public channel link."))
- }
- proto::ChannelVisibility::Members => {
- (Some("icons/hash.svg"), Some("Copy private channel link."))
- }
- };
-
- Some(channel.name.as_str())
- });
-
- if let Some(name) = channel_name {
- Cow::Owned(format!("{}", name))
- } else {
- Cow::Borrowed("Current Call")
- }
- }
- Section::ContactRequests => Cow::Borrowed("Requests"),
- Section::Contacts => Cow::Borrowed("Contacts"),
- Section::Channels => Cow::Borrowed("Channels"),
- Section::ChannelInvites => Cow::Borrowed("Invites"),
- Section::Online => Cow::Borrowed("Online"),
- Section::Offline => Cow::Borrowed("Offline"),
- };
-
- enum AddContact {}
- let button = match section {
- Section::ActiveCall => channel_link.map(|channel_link| {
- let channel_link_copy = channel_link.clone();
- MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
- render_icon_button(
- theme
- .collab_panel
- .leave_call_button
- .style_for(is_selected, state),
- "icons/link.svg",
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- let item = ClipboardItem::new(channel_link_copy.clone());
- cx.write_to_clipboard(item)
- })
- .with_tooltip::<AddContact>(
- 0,
- channel_tooltip_text.unwrap(),
- None,
- tooltip_style.clone(),
- cx,
- )
- }),
- Section::Contacts => Some(
- MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
- render_icon_button(
- theme
- .collab_panel
- .add_contact_button
- .style_for(is_selected, state),
- "icons/plus.svg",
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, this, cx| {
- this.toggle_contact_finder(cx);
- })
- .with_tooltip::<LeaveCallContactList>(
- 0,
- "Search for new contact",
- None,
- tooltip_style.clone(),
- cx,
- ),
- ),
- Section::Channels => {
- if cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<Channel>(cx.window())
- .is_some()
- && self.drag_target_channel == ChannelDragTarget::Root
- {
- is_dragged_over = true;
- }
-
- Some(
- MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
- render_icon_button(
- theme
- .collab_panel
- .add_contact_button
- .style_for(is_selected, state),
- "icons/plus.svg",
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
- .with_tooltip::<AddChannel>(
- 0,
- "Create a channel",
- None,
- tooltip_style.clone(),
- cx,
- ),
- )
- }
- _ => None,
- };
-
- let can_collapse = match section {
- Section::ActiveCall | Section::Channels | Section::Contacts => false,
- Section::ChannelInvites
- | Section::ContactRequests
- | Section::Online
- | Section::Offline => true,
- };
- let icon_size = (&theme.collab_panel).section_icon_size;
- let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
- let header_style = if can_collapse {
- theme
- .collab_panel
- .subheader_row
- .in_state(is_selected)
- .style_for(state)
- } else {
- &theme.collab_panel.header_row
- };
-
- Flex::row()
- .with_children(if can_collapse {
- Some(
- Svg::new(if is_collapsed {
- "icons/chevron_right.svg"
- } else {
- "icons/chevron_down.svg"
- })
- .with_color(header_style.text.color)
- .constrained()
- .with_max_width(icon_size)
- .with_max_height(icon_size)
- .aligned()
- .constrained()
- .with_width(icon_size)
- .contained()
- .with_margin_right(
- theme.collab_panel.contact_username.container.margin.left,
- ),
- )
- } else if let Some(channel_icon) = channel_icon {
- Some(
- Svg::new(channel_icon)
- .with_color(header_style.text.color)
- .constrained()
- .with_max_width(icon_size)
- .with_max_height(icon_size)
- .aligned()
- .constrained()
- .with_width(icon_size)
- .contained()
- .with_margin_right(
- theme.collab_panel.contact_username.container.margin.left,
- ),
- )
- } else {
- None
- })
- .with_child(
- Label::new(text, header_style.text.clone())
- .aligned()
- .left()
- .flex(1., true),
- )
- .with_children(button.map(|button| button.aligned().right()))
- .constrained()
- .with_height(theme.collab_panel.row_height)
- .contained()
- .with_style(if is_dragged_over {
- theme.collab_panel.dragged_over_header
- } else {
- header_style.container
- })
- });
-
- result = result
- .on_move(move |_, this, cx| {
- if cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<Channel>(cx.window())
- .is_some()
- {
- this.drag_target_channel = ChannelDragTarget::Root;
- cx.notify()
- }
- })
- .on_up(MouseButton::Left, move |_, this, cx| {
- if let Some((_, dragged_channel)) = cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<Channel>(cx.window())
- {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(dragged_channel.id, None, cx)
- })
- .detach_and_log_err(cx)
- }
- });
-
- if can_collapse {
- result = result
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if can_collapse {
- this.toggle_section_expanded(section, cx);
- }
- })
- }
-
- result.into_any()
- }
-
- fn render_contact(
- contact: &Contact,
- calling: bool,
- project: &ModelHandle<Project>,
- theme: &theme::Theme,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum ContactTooltip {}
-
- let collab_theme = &theme.collab_panel;
- let online = contact.online;
- let busy = contact.busy || calling;
- let user_id = contact.user.id;
- let github_login = contact.user.github_login.clone();
- let initial_project = project.clone();
-
- let event_handler =
- MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
- Flex::row()
- .with_children(contact.user.avatar.clone().map(|avatar| {
- let status_badge = if contact.online {
- Some(
- Empty::new()
- .collapsed()
- .contained()
- .with_style(if busy {
- collab_theme.contact_status_busy
- } else {
- collab_theme.contact_status_free
- })
- .aligned(),
- )
- } else {
- None
- };
- Stack::new()
- .with_child(
- Image::from_data(avatar)
- .with_style(collab_theme.contact_avatar)
- .aligned()
- .left(),
- )
- .with_children(status_badge)
- }))
- .with_child(
- Label::new(
- contact.user.github_login.clone(),
- collab_theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(collab_theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .with_children(if state.hovered() {
- Some(
- MouseEventHandler::new::<Cancel, _>(
- contact.user.id as usize,
- cx,
- |mouse_state, _| {
- let button_style =
- collab_theme.contact_button.style_for(mouse_state);
- render_icon_button(button_style, "icons/x.svg")
- .aligned()
- .flex_float()
- },
- )
- .with_padding(Padding::uniform(2.))
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.remove_contact(user_id, &github_login, cx);
- })
- .flex_float(),
- )
- } else {
- None
- })
- .with_children(if calling {
- Some(
- Label::new("Calling", collab_theme.calling_indicator.text.clone())
- .contained()
- .with_style(collab_theme.calling_indicator.container)
- .aligned(),
- )
- } else {
- None
- })
- .constrained()
- .with_height(collab_theme.row_height)
- .contained()
- .with_style(
- *collab_theme
- .contact_row
- .in_state(is_selected)
- .style_for(state),
- )
- });
-
- if online && !busy {
- let room = ActiveCall::global(cx).read(cx).room();
- let label = if room.is_some() {
- format!("Invite {} to join call", contact.user.github_login)
- } else {
- format!("Call {}", contact.user.github_login)
- };
-
- event_handler
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.call(user_id, Some(initial_project.clone()), cx);
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .with_tooltip::<ContactTooltip>(
- contact.user.id as usize,
- label,
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- } else {
- event_handler
- .with_tooltip::<ContactTooltip>(
- contact.user.id as usize,
- format!(
- "{} is {}",
- contact.user.github_login,
- if busy { "on a call" } else { "offline" }
- ),
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- }
- }
-
- fn render_contact_placeholder(
- &self,
- theme: &theme::CollabPanel,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum AddContacts {}
- MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
- let style = theme.list_empty_state.style_for(is_selected, state);
- Flex::row()
- .with_child(
- Svg::new("icons/plus.svg")
- .with_color(theme.list_empty_icon.color)
- .constrained()
- .with_width(theme.list_empty_icon.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new("Add a contact", style.text.clone())
- .contained()
- .with_style(theme.list_empty_label_container),
- )
- .align_children_center()
- .contained()
- .with_style(style.container)
- .into_any()
- })
- .on_click(MouseButton::Left, |_, this, cx| {
- this.toggle_contact_finder(cx);
- })
- .into_any()
- }
-
- fn render_channel_editor(
- &self,
- theme: &theme::Theme,
- depth: usize,
- cx: &AppContext,
- ) -> AnyElement<Self> {
- Flex::row()
- .with_child(
- Empty::new()
- .constrained()
- .with_width(theme.collab_panel.disclosure.button_space()),
- )
- .with_child(
- Svg::new("icons/hash.svg")
- .with_color(theme.collab_panel.channel_hash.color)
- .constrained()
- .with_width(theme.collab_panel.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- if let Some(pending_name) = self
- .channel_editing_state
- .as_ref()
- .and_then(|state| state.pending_name())
- {
- Label::new(
- pending_name.to_string(),
- theme.collab_panel.contact_username.text.clone(),
- )
- .contained()
- .with_style(theme.collab_panel.contact_username.container)
- .aligned()
- .left()
- .flex(1., true)
- .into_any()
- } else {
- ChildView::new(&self.channel_name_editor, cx)
- .aligned()
- .left()
- .contained()
- .with_style(theme.collab_panel.channel_editor)
- .flex(1.0, true)
- .into_any()
- },
- )
- .align_children_center()
- .constrained()
- .with_height(theme.collab_panel.row_height)
- .contained()
- .with_style(ContainerStyle {
- background_color: Some(theme.editor.background),
- ..*theme.collab_panel.contact_row.default_style()
- })
- .with_padding_left(
- theme.collab_panel.contact_row.default_style().padding.left
- + theme.collab_panel.channel_indent * depth as f32,
- )
- .into_any()
- }
-
- fn render_channel(
- &self,
- channel: &Channel,
- depth: usize,
- theme: &theme::Theme,
- is_selected: bool,
- has_children: bool,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let channel_id = channel.id;
- let collab_theme = &theme.collab_panel;
- let is_public = self
- .channel_store
- .read(cx)
- .channel_for_id(channel_id)
- .map(|channel| channel.visibility)
- == Some(proto::ChannelVisibility::Public);
- let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
- let disclosed =
- has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
-
- let is_active = maybe!({
- let call_channel = ActiveCall::global(cx)
- .read(cx)
- .room()?
- .read(cx)
- .channel_id()?;
- Some(call_channel == channel_id)
- })
- .unwrap_or(false);
-
- const FACEPILE_LIMIT: usize = 3;
-
- enum ChannelCall {}
- enum ChannelNote {}
- enum NotesTooltip {}
- enum ChatTooltip {}
- enum ChannelTooltip {}
-
- let mut is_dragged_over = false;
- if cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<Channel>(cx.window())
- .is_some()
- && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
- {
- is_dragged_over = true;
- }
-
- let has_messages_notification = channel.unseen_message_id.is_some();
-
- MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
- let row_hovered = state.hovered();
-
- let mut select_state = |interactive: &Interactive<ContainerStyle>| {
- if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
- interactive.clicked.as_ref().unwrap().clone()
- } else if state.hovered() || other_selected {
- interactive
- .hovered
- .as_ref()
- .unwrap_or(&interactive.default)
- .clone()
- } else {
- interactive.default.clone()
- }
- };
-
- Flex::<Self>::row()
- .with_child(
- Svg::new(if is_public {
- "icons/public.svg"
- } else {
- "icons/hash.svg"
- })
- .with_color(collab_theme.channel_hash.color)
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child({
- let style = collab_theme.channel_name.inactive_state();
- Flex::row()
- .with_child(
- Label::new(channel.name.clone(), style.text.clone())
- .contained()
- .with_style(style.container)
- .aligned()
- .left()
- .with_tooltip::<ChannelTooltip>(
- ix,
- "Join channel",
- None,
- theme.tooltip.clone(),
- cx,
- ),
- )
- .with_children({
- let participants =
- self.channel_store.read(cx).channel_participants(channel_id);
-
- if !participants.is_empty() {
- let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
-
- let result = FacePile::new(collab_theme.face_overlap)
- .with_children(
- participants
- .iter()
- .filter_map(|user| {
- Some(
- Image::from_data(user.avatar.clone()?)
- .with_style(collab_theme.channel_avatar),
- )
- })
- .take(FACEPILE_LIMIT),
- )
- .with_children((extra_count > 0).then(|| {
- Label::new(
- format!("+{}", extra_count),
- collab_theme.extra_participant_label.text.clone(),
- )
- .contained()
- .with_style(collab_theme.extra_participant_label.container)
- }));
-
- Some(result)
- } else {
- None
- }
- })
- .with_spacing(8.)
- .align_children_center()
- .flex(1., true)
- })
- .with_child(
- MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
- let container_style = collab_theme
- .disclosure
- .button
- .style_for(mouse_state)
- .container;
-
- if channel.unseen_message_id.is_some() {
- Svg::new("icons/conversations.svg")
- .with_color(collab_theme.channel_note_active_color)
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .contained()
- .with_style(container_style)
- .with_uniform_padding(4.)
- .into_any()
- } else if row_hovered {
- Svg::new("icons/conversations.svg")
- .with_color(collab_theme.channel_hash.color)
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .contained()
- .with_style(container_style)
- .with_uniform_padding(4.)
- .into_any()
- } else {
- Empty::new().into_any()
- }
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
- })
- .with_tooltip::<ChatTooltip>(
- ix,
- "Open channel chat",
- None,
- theme.tooltip.clone(),
- cx,
- )
- .contained()
- .with_margin_right(4.),
- )
- .with_child(
- MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
- let container_style = collab_theme
- .disclosure
- .button
- .style_for(mouse_state)
- .container;
- if row_hovered || channel.unseen_note_version.is_some() {
- Svg::new("icons/file.svg")
- .with_color(if channel.unseen_note_version.is_some() {
- collab_theme.channel_note_active_color
- } else {
- collab_theme.channel_hash.color
- })
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .contained()
- .with_style(container_style)
- .with_uniform_padding(4.)
- .with_margin_right(collab_theme.channel_hash.container.margin.left)
- .with_tooltip::<NotesTooltip>(
- ix as usize,
- "Open channel notes",
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- } else if has_messages_notification {
- Empty::new()
- .constrained()
- .with_width(collab_theme.channel_hash.width)
- .contained()
- .with_uniform_padding(4.)
- .with_margin_right(collab_theme.channel_hash.container.margin.left)
- .into_any()
- } else {
- Empty::new().into_any()
- }
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
- }),
- )
- .align_children_center()
- .styleable_component()
- .disclosable(
- disclosed,
- Box::new(ToggleCollapse {
- location: channel.id.clone(),
- }),
- )
- .with_id(ix)
- .with_style(collab_theme.disclosure.clone())
- .element()
- .constrained()
- .with_height(collab_theme.row_height)
- .contained()
- .with_style(select_state(
- collab_theme
- .channel_row
- .in_state(is_selected || is_active || is_dragged_over),
- ))
- .with_padding_left(
- collab_theme.channel_row.default_style().padding.left
- + collab_theme.channel_indent * depth as f32,
- )
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- if this.drag_target_channel == ChannelDragTarget::None {
- if is_active {
- this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
- } else {
- this.join_channel(channel_id, cx)
- }
- }
- })
- .on_click(MouseButton::Right, {
- let channel = channel.clone();
- move |e, this, cx| {
- this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
- }
- })
- .on_up(MouseButton::Left, move |_, this, cx| {
- if let Some((_, dragged_channel)) = cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<Channel>(cx.window())
- {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
- })
- .detach_and_log_err(cx)
- }
- })
- .on_move({
- let channel = channel.clone();
- move |_, this, cx| {
- if let Some((_, dragged_channel)) = cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<Channel>(cx.window())
- {
- if channel.id != dragged_channel.id {
- this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
- }
- cx.notify()
- }
- }
- })
- .as_draggable::<_, Channel>(
- channel.clone(),
- move |_, channel, cx: &mut ViewContext<Workspace>| {
- let theme = &theme::current(cx).collab_panel;
-
- Flex::<Workspace>::row()
- .with_child(
- Svg::new("icons/hash.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new(channel.name.clone(), theme.channel_name.text.clone())
- .contained()
- .with_style(theme.channel_name.container)
- .aligned()
- .left(),
- )
- .align_children_center()
- .contained()
- .with_background_color(
- theme
- .container
- .background_color
- .unwrap_or(gpui::color::Color::transparent_black()),
- )
- .contained()
- .with_padding_left(
- theme.channel_row.default_style().padding.left
- + theme.channel_indent * depth as f32,
- )
- .into_any()
- },
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .into_any()
- }
-
- fn render_channel_notes(
- &self,
- channel_id: ChannelId,
- theme: &theme::CollabPanel,
- is_selected: bool,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum ChannelNotes {}
- let host_avatar_width = theme
- .contact_avatar
- .width
- .or(theme.contact_avatar.height)
- .unwrap_or(0.);
-
- MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
- let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
- let row = theme.project_row.in_state(is_selected).style_for(state);
-
- Flex::<Self>::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- false,
- vec2f(host_avatar_width, theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/file.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new("notes", theme.channel_name.text.clone())
- .contained()
- .with_style(theme.channel_name.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(*theme.channel_row.style_for(is_selected, state))
- .with_padding_left(theme.channel_row.default_style().padding.left)
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .into_any()
- }
-
- fn render_channel_chat(
- &self,
- channel_id: ChannelId,
- theme: &theme::CollabPanel,
- is_selected: bool,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum ChannelChat {}
- let host_avatar_width = theme
- .contact_avatar
- .width
- .or(theme.contact_avatar.height)
- .unwrap_or(0.);
-
- MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
- let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
- let row = theme.project_row.in_state(is_selected).style_for(state);
-
- Flex::<Self>::row()
- .with_child(render_tree_branch(
- tree_branch,
- &row.name.text,
- true,
- vec2f(host_avatar_width, theme.row_height),
- cx.font_cache(),
- ))
- .with_child(
- Svg::new("icons/conversations.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new("chat", theme.channel_name.text.clone())
- .contained()
- .with_style(theme.channel_name.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(*theme.channel_row.style_for(is_selected, state))
- .with_padding_left(theme.channel_row.default_style().padding.left)
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .into_any()
- }
-
- fn render_channel_invite(
- channel: Arc<Channel>,
- channel_store: ModelHandle<ChannelStore>,
- theme: &theme::CollabPanel,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum Decline {}
- enum Accept {}
-
- let channel_id = channel.id;
- let is_invite_pending = channel_store
- .read(cx)
- .has_pending_channel_invite_response(&channel);
- let button_spacing = theme.contact_button_spacing;
-
- Flex::row()
- .with_child(
- Svg::new("icons/hash.svg")
- .with_color(theme.channel_hash.color)
- .constrained()
- .with_width(theme.channel_hash.width)
- .aligned()
- .left(),
- )
- .with_child(
- Label::new(channel.name.clone(), theme.contact_username.text.clone())
- .contained()
- .with_style(theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true),
- )
- .with_child(
- MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
- let button_style = if is_invite_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state)
- };
- render_icon_button(button_style, "icons/x.svg").aligned()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.respond_to_channel_invite(channel_id, false, cx);
- })
- .contained()
- .with_margin_right(button_spacing),
- )
- .with_child(
- MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
- let button_style = if is_invite_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state)
- };
- render_icon_button(button_style, "icons/check.svg")
- .aligned()
- .flex_float()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.respond_to_channel_invite(channel_id, true, cx);
- }),
- )
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(
- *theme
- .contact_row
- .in_state(is_selected)
- .style_for(&mut Default::default()),
- )
- .with_padding_left(
- theme.contact_row.default_style().padding.left + theme.channel_indent,
- )
- .into_any()
- }
-
- fn render_contact_request(
- user: Arc<User>,
- user_store: ModelHandle<UserStore>,
- theme: &theme::CollabPanel,
- is_incoming: bool,
- is_selected: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum Decline {}
- enum Accept {}
- enum Cancel {}
-
- let mut row = Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(
- user.github_login.clone(),
- theme.contact_username.text.clone(),
- )
- .contained()
- .with_style(theme.contact_username.container)
- .aligned()
- .left()
- .flex(1., true),
- );
-
- let user_id = user.id;
- let github_login = user.github_login.clone();
- let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
- let button_spacing = theme.contact_button_spacing;
-
- if is_incoming {
- row.add_child(
- MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
- let button_style = if is_contact_request_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state)
- };
- render_icon_button(button_style, "icons/x.svg").aligned()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.respond_to_contact_request(user_id, false, cx);
- })
- .contained()
- .with_margin_right(button_spacing),
- );
-
- row.add_child(
- MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
- let button_style = if is_contact_request_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state)
- };
- render_icon_button(button_style, "icons/check.svg")
- .aligned()
- .flex_float()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.respond_to_contact_request(user_id, true, cx);
- }),
- );
- } else {
- row.add_child(
- MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
- let button_style = if is_contact_request_pending {
- &theme.disabled_button
- } else {
- theme.contact_button.style_for(mouse_state)
- };
- render_icon_button(button_style, "icons/x.svg")
- .aligned()
- .flex_float()
- })
- .with_padding(Padding::uniform(2.))
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.remove_contact(user_id, &github_login, cx);
- })
- .flex_float(),
- );
- }
-
- row.constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(
- *theme
- .contact_row
- .in_state(is_selected)
- .style_for(&mut Default::default()),
- )
- .into_any()
- }
-
- fn has_subchannels(&self, ix: usize) -> bool {
- self.entries.get(ix).map_or(false, |entry| {
- if let ListEntry::Channel { has_children, .. } = entry {
- *has_children
- } else {
- false
- }
- })
- }
-
- fn deploy_channel_context_menu(
- &mut self,
- position: Option<Vector2F>,
- channel: &Channel,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) {
- self.context_menu_on_selected = position.is_none();
-
- let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
- self.channel_store
- .read(cx)
- .channel_for_id(clipboard.channel_id)
- .map(|channel| channel.name.clone())
- });
-
- self.context_menu.update(cx, |context_menu, cx| {
- context_menu.set_position_mode(if self.context_menu_on_selected {
- OverlayPositionMode::Local
- } else {
- OverlayPositionMode::Window
- });
-
- let mut items = Vec::new();
-
- let select_action_name = if self.selection == Some(ix) {
- "Unselect"
- } else {
- "Select"
- };
-
- items.push(ContextMenuItem::action(
- select_action_name,
- ToggleSelectedIx { ix },
- ));
-
- if self.has_subchannels(ix) {
- let expand_action_name = if self.is_channel_collapsed(channel.id) {
- "Expand Subchannels"
- } else {
- "Collapse Subchannels"
- };
- items.push(ContextMenuItem::action(
- expand_action_name,
- ToggleCollapse {
- location: channel.id,
- },
- ));
- }
-
- items.push(ContextMenuItem::action(
- "Open Notes",
- OpenChannelNotes {
- channel_id: channel.id,
- },
- ));
-
- items.push(ContextMenuItem::action(
- "Open Chat",
- JoinChannelChat {
- channel_id: channel.id,
- },
- ));
-
- items.push(ContextMenuItem::action(
- "Copy Channel Link",
- CopyChannelLink {
- channel_id: channel.id,
- },
- ));
-
- if self.channel_store.read(cx).is_channel_admin(channel.id) {
- items.extend([
- ContextMenuItem::Separator,
- ContextMenuItem::action(
- "New Subchannel",
- NewChannel {
- location: channel.id,
- },
- ),
- ContextMenuItem::action(
- "Rename",
- RenameChannel {
- channel_id: channel.id,
- },
- ),
- ContextMenuItem::action(
- "Move this channel",
- StartMoveChannelFor {
- channel_id: channel.id,
- },
- ),
- ]);
-
- if let Some(channel_name) = clipboard_channel_name {
- items.push(ContextMenuItem::Separator);
- items.push(ContextMenuItem::action(
- format!("Move '#{}' here", channel_name),
- MoveChannel { to: channel.id },
- ));
- }
-
- items.extend([
- ContextMenuItem::Separator,
- ContextMenuItem::action(
- "Invite Members",
- InviteMembers {
- channel_id: channel.id,
- },
- ),
- ContextMenuItem::action(
- "Manage Members",
- ManageMembers {
- channel_id: channel.id,
- },
- ),
- ContextMenuItem::Separator,
- ContextMenuItem::action(
- "Delete",
- RemoveChannel {
- channel_id: channel.id,
- },
- ),
- ]);
- }
-
- context_menu.show(
- position.unwrap_or_default(),
- if self.context_menu_on_selected {
- gpui::elements::AnchorCorner::TopRight
- } else {
- gpui::elements::AnchorCorner::BottomLeft
- },
- items,
- cx,
- );
- });
-
- cx.notify();
- }
-
- fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- if self.take_editing_state(cx) {
- cx.focus(&self.filter_editor);
- } else {
- self.filter_editor.update(cx, |editor, cx| {
- if editor.buffer().read(cx).len(cx) > 0 {
- editor.set_text("", cx);
- }
- });
- }
-
- self.update_entries(false, cx);
- }
-
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- let ix = self.selection.map_or(0, |ix| ix + 1);
- if ix < self.entries.len() {
- self.selection = Some(ix);
- }
-
- self.list_state.reset(self.entries.len());
- if let Some(ix) = self.selection {
- self.list_state.scroll_to(ListOffset {
- item_ix: ix,
- offset_in_item: 0.,
- });
- }
- cx.notify();
- }
-
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- let ix = self.selection.take().unwrap_or(0);
- if ix > 0 {
- self.selection = Some(ix - 1);
- }
-
- self.list_state.reset(self.entries.len());
- if let Some(ix) = self.selection {
- self.list_state.scroll_to(ListOffset {
- item_ix: ix,
- offset_in_item: 0.,
- });
- }
- cx.notify();
- }
-
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- if self.confirm_channel_edit(cx) {
- return;
- }
-
- if let Some(selection) = self.selection {
- if let Some(entry) = self.entries.get(selection) {
- match entry {
- ListEntry::Header(section) => match section {
- Section::ActiveCall => Self::leave_call(cx),
- Section::Channels => self.new_root_channel(cx),
- Section::Contacts => self.toggle_contact_finder(cx),
- Section::ContactRequests
- | Section::Online
- | Section::Offline
- | Section::ChannelInvites => {
- self.toggle_section_expanded(*section, cx);
- }
- },
- ListEntry::Contact { contact, calling } => {
- if contact.online && !contact.busy && !calling {
- self.call(contact.user.id, Some(self.project.clone()), cx);
- }
- }
- ListEntry::ParticipantProject {
- project_id,
- host_user_id,
- ..
- } => {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_remote_project(
- *project_id,
- *host_user_id,
- app_state,
- cx,
- )
- .detach_and_log_err(cx);
- }
- }
- ListEntry::ParticipantScreen { peer_id, .. } => {
- let Some(peer_id) = peer_id else {
- return;
- };
- if let Some(workspace) = self.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace.open_shared_screen(*peer_id, cx)
- });
- }
- }
- ListEntry::Channel { channel, .. } => {
- let is_active = maybe!({
- let call_channel = ActiveCall::global(cx)
- .read(cx)
- .room()?
- .read(cx)
- .channel_id()?;
-
- Some(call_channel == channel.id)
- })
- .unwrap_or(false);
- if is_active {
- self.open_channel_notes(
- &OpenChannelNotes {
- channel_id: channel.id,
- },
- cx,
- )
- } else {
- self.join_channel(channel.id, cx)
- }
- }
- ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
- _ => {}
- }
- }
- }
- }
-
- fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
- if self.channel_editing_state.is_some() {
- self.channel_name_editor.update(cx, |editor, cx| {
- editor.insert(" ", cx);
- });
- }
- }
-
- fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
- if let Some(editing_state) = &mut self.channel_editing_state {
- match editing_state {
- ChannelEditingState::Create {
- location,
- pending_name,
- ..
- } => {
- if pending_name.is_some() {
- return false;
- }
- let channel_name = self.channel_name_editor.read(cx).text(cx);
-
- *pending_name = Some(channel_name.clone());
-
- self.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.create_channel(&channel_name, *location, cx)
- })
- .detach();
- cx.notify();
- }
- ChannelEditingState::Rename {
- location,
- pending_name,
- } => {
- if pending_name.is_some() {
- return false;
- }
- let channel_name = self.channel_name_editor.read(cx).text(cx);
- *pending_name = Some(channel_name.clone());
-
- self.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.rename(*location, &channel_name, cx)
- })
- .detach();
- cx.notify();
- }
- }
- cx.focus_self();
- true
- } else {
- false
- }
- }
-
- fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
- if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
- self.collapsed_sections.remove(ix);
- } else {
- self.collapsed_sections.push(section);
- }
- self.update_entries(false, cx);
- }
-
- fn collapse_selected_channel(
- &mut self,
- _: &CollapseSelectedChannel,
- cx: &mut ViewContext<Self>,
- ) {
- let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
- return;
- };
-
- if self.is_channel_collapsed(channel_id) {
- return;
- }
-
- self.toggle_channel_collapsed(channel_id, cx);
- }
-
- fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
- let Some(id) = self.selected_channel().map(|channel| channel.id) else {
- return;
- };
-
- if !self.is_channel_collapsed(id) {
- return;
- }
-
- self.toggle_channel_collapsed(id, cx)
- }
-
- fn toggle_channel_collapsed_action(
- &mut self,
- action: &ToggleCollapse,
- cx: &mut ViewContext<Self>,
- ) {
- self.toggle_channel_collapsed(action.location, cx);
- }
-
- fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- match self.collapsed_channels.binary_search(&channel_id) {
- Ok(ix) => {
- self.collapsed_channels.remove(ix);
- }
- Err(ix) => {
- self.collapsed_channels.insert(ix, channel_id);
- }
- };
- self.serialize(cx);
- self.update_entries(true, cx);
- cx.notify();
- cx.focus_self();
- }
-
- fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
- self.collapsed_channels.binary_search(&channel_id).is_ok()
- }
-
- fn leave_call(cx: &mut ViewContext<Self>) {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
- }
-
- fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- workspace.toggle_modal(cx, |_, cx| {
- cx.add_view(|cx| {
- let mut finder = ContactFinder::new(self.user_store.clone(), cx);
- finder.set_query(self.filter_editor.read(cx).text(cx), cx);
- finder
- })
- });
- });
- }
- }
-
- fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
- self.channel_editing_state = Some(ChannelEditingState::Create {
- location: None,
- pending_name: None,
- });
- self.update_entries(false, cx);
- self.select_channel_editor();
- cx.focus(self.channel_name_editor.as_any());
- cx.notify();
- }
-
- fn select_channel_editor(&mut self) {
- self.selection = self.entries.iter().position(|entry| match entry {
- ListEntry::ChannelEditor { .. } => true,
- _ => false,
- });
- }
-
- fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
- self.collapsed_channels
- .retain(|channel| *channel != action.location);
- self.channel_editing_state = Some(ChannelEditingState::Create {
- location: Some(action.location.to_owned()),
- pending_name: None,
- });
- self.update_entries(false, cx);
- self.select_channel_editor();
- cx.focus(self.channel_name_editor.as_any());
- cx.notify();
- }
-
- fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
- self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
- }
-
- fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
- self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
- }
-
- fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
- if let Some(channel) = self.selected_channel() {
- self.remove_channel(channel.id, cx)
- }
- }
-
- fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
- if let Some(channel) = self.selected_channel() {
- self.rename_channel(
- &RenameChannel {
- channel_id: channel.id,
- },
- cx,
- );
- }
- }
-
- fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.read(cx);
- if !channel_store.is_channel_admin(action.channel_id) {
- return;
- }
- if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
- self.channel_editing_state = Some(ChannelEditingState::Rename {
- location: action.channel_id.to_owned(),
- pending_name: None,
- });
- self.channel_name_editor.update(cx, |editor, cx| {
- editor.set_text(channel.name.clone(), cx);
- editor.select_all(&Default::default(), cx);
- });
- cx.focus(self.channel_name_editor.as_any());
- self.update_entries(false, cx);
- self.select_channel_editor();
- }
- }
-
- fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- ChannelView::open(action.channel_id, workspace, cx).detach();
- }
- }
-
- fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
- let Some(channel) = self.selected_channel() else {
- return;
- };
-
- self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
- }
-
- fn selected_channel(&self) -> Option<&Arc<Channel>> {
- self.selection
- .and_then(|ix| self.entries.get(ix))
- .and_then(|entry| match entry {
- ListEntry::Channel { channel, .. } => Some(channel),
- _ => None,
- })
- }
-
- fn show_channel_modal(
- &mut self,
- channel_id: ChannelId,
- mode: channel_modal::Mode,
- cx: &mut ViewContext<Self>,
- ) {
- let workspace = self.workspace.clone();
- let user_store = self.user_store.clone();
- let channel_store = self.channel_store.clone();
- let members = self.channel_store.update(cx, |channel_store, cx| {
- channel_store.get_channel_member_details(channel_id, cx)
- });
-
- cx.spawn(|_, mut cx| async move {
- let members = members.await?;
- workspace.update(&mut cx, |workspace, cx| {
- workspace.toggle_modal(cx, |_, cx| {
- cx.add_view(|cx| {
- ChannelModal::new(
- user_store.clone(),
- channel_store.clone(),
- channel_id,
- mode,
- members,
- cx,
- )
- })
- });
- })
- })
- .detach();
- }
-
- fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
- self.remove_channel(action.channel_id, cx)
- }
-
- fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.clone();
- if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
- let prompt_message = format!(
- "Are you sure you want to remove the channel \"{}\"?",
- channel.name
- );
- let mut answer =
- cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
- let window = cx.window();
- cx.spawn(|this, mut cx| async move {
- if answer.next().await == Some(0) {
- if let Err(e) = channel_store
- .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
- .await
- {
- window.prompt(
- PromptLevel::Info,
- &format!("Failed to remove channel: {}", e),
- &["Ok"],
- &mut cx,
- );
- }
- this.update(&mut cx, |_, cx| cx.focus_self()).ok();
- }
- })
- .detach();
- }
- }
-
- // Should move to the filter editor if clicking on it
- // Should move selection to the channel editor if activating it
-
- fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
- let user_store = self.user_store.clone();
- let prompt_message = format!(
- "Are you sure you want to remove \"{}\" from your contacts?",
- github_login
- );
- let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
- let window = cx.window();
- cx.spawn(|_, mut cx| async move {
- if answer.next().await == Some(0) {
- if let Err(e) = user_store
- .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
- .await
- {
- window.prompt(
- PromptLevel::Info,
- &format!("Failed to remove contact: {}", e),
- &["Ok"],
- &mut cx,
- );
- }
- }
- })
- .detach();
- }
-
- fn respond_to_contact_request(
- &mut self,
- user_id: u64,
- accept: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.user_store
- .update(cx, |store, cx| {
- store.respond_to_contact_request(user_id, accept, cx)
- })
- .detach();
- }
-
- fn respond_to_channel_invite(
- &mut self,
- channel_id: u64,
- accept: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.channel_store
- .update(cx, |store, cx| {
- store.respond_to_channel_invite(channel_id, accept, cx)
- })
- .detach();
- }
-
- fn call(
- &mut self,
- recipient_user_id: u64,
- initial_project: Option<ModelHandle<Project>>,
- cx: &mut ViewContext<Self>,
- ) {
- ActiveCall::global(cx)
- .update(cx, |call, cx| {
- call.invite(recipient_user_id, initial_project, cx)
- })
- .detach_and_log_err(cx);
- }
-
- fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
- let Some(workspace) = self.workspace.upgrade(cx) else {
- return;
- };
- let Some(handle) = cx.window().downcast::<Workspace>() else {
- return;
- };
- workspace::join_channel(
- channel_id,
- workspace.read(cx).app_state().clone(),
- Some(handle),
- cx,
- )
- .detach_and_log_err(cx)
- }
-
- fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
- let channel_id = action.channel_id;
- if let Some(workspace) = self.workspace.upgrade(cx) {
- cx.app_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
- panel.update(cx, |panel, cx| {
- panel
- .select_channel(channel_id, None, cx)
- .detach_and_log_err(cx);
- });
- }
- });
- });
- }
- }
-
- fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
- let channel_store = self.channel_store.read(cx);
- let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
- return;
- };
- let item = ClipboardItem::new(channel.link());
- cx.write_to_clipboard(item)
- }
-}
-
-fn render_tree_branch(
- branch_style: theme::TreeBranch,
- row_style: &TextStyle,
- is_last: bool,
- size: Vector2F,
- font_cache: &FontCache,
-) -> gpui::elements::ConstrainedBox<CollabPanel> {
- let line_height = row_style.line_height(font_cache);
- let cap_height = row_style.cap_height(font_cache);
- let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
-
- Canvas::new(move |bounds, _, _, cx| {
- cx.paint_layer(None, |cx| {
- let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
- let end_x = bounds.max_x();
- let start_y = bounds.min_y();
- let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
-
- cx.scene().push_quad(gpui::Quad {
- bounds: RectF::from_points(
- vec2f(start_x, start_y),
- vec2f(
- start_x + branch_style.width,
- if is_last { end_y } else { bounds.max_y() },
- ),
- ),
- background: Some(branch_style.color),
- border: gpui::Border::default(),
- corner_radii: (0.).into(),
- });
- cx.scene().push_quad(gpui::Quad {
- bounds: RectF::from_points(
- vec2f(start_x, end_y),
- vec2f(end_x, end_y + branch_style.width),
- ),
- background: Some(branch_style.color),
- border: gpui::Border::default(),
- corner_radii: (0.).into(),
- });
- })
- })
- .constrained()
- .with_width(size.x())
-}
-
-impl View for CollabPanel {
- fn ui_name() -> &'static str {
- "CollabPanel"
- }
-
- fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
- if !self.has_focus {
- self.has_focus = true;
- if !self.context_menu.is_focused(cx) {
- if let Some(editing_state) = &self.channel_editing_state {
- if editing_state.pending_name().is_none() {
- cx.focus(&self.channel_name_editor);
- } else {
- cx.focus(&self.filter_editor);
- }
- } else {
- cx.focus(&self.filter_editor);
- }
- }
- cx.emit(Event::Focus);
- }
- }
-
- fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
- }
-
- fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
- let theme = &theme::current(cx).collab_panel;
-
- if self.user_store.read(cx).current_user().is_none() {
- enum LogInButton {}
-
- return Flex::column()
- .with_child(
- MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
- let button = theme.log_in_button.style_for(state);
- Label::new("Sign in to collaborate", button.text.clone())
- .aligned()
- .left()
- .contained()
- .with_style(button.container)
- })
- .on_click(MouseButton::Left, |_, this, cx| {
- let client = this.client.clone();
- cx.spawn(|_, cx| async move {
- client.authenticate_and_connect(true, &cx).await.log_err();
- })
- .detach();
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .contained()
- .with_style(theme.container)
- .into_any();
- }
-
- enum PanelFocus {}
- MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
- Stack::new()
- .with_child(
- Flex::column()
- .with_child(
- Flex::row().with_child(
- ChildView::new(&self.filter_editor, cx)
- .contained()
- .with_style(theme.user_query_editor.container)
- .flex(1.0, true),
- ),
- )
- .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
- .contained()
- .with_style(theme.container)
- .into_any(),
- )
- .with_children(
- (!self.context_menu_on_selected)
- .then(|| ChildView::new(&self.context_menu, cx)),
- )
- .into_any()
- })
- .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
- .into_any_named("collab panel")
- }
-
- fn update_keymap_context(
- &self,
- keymap: &mut gpui::keymap_matcher::KeymapContext,
- _: &AppContext,
- ) {
- Self::reset_to_default_keymap_context(keymap);
- if self.channel_editing_state.is_some() {
- keymap.add_identifier("editing");
- } else {
- keymap.add_identifier("not_editing");
- }
- }
-}
-
-impl Panel for CollabPanel {
- fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- settings::get::<CollaborationPanelSettings>(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::<CollaborationPanelSettings>(
- self.fs.clone(),
- cx,
- move |settings| settings.dock = Some(position),
- );
- }
-
- fn size(&self, cx: &gpui::WindowContext) -> f32 {
- self.width
- .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
- self.width = size;
- self.serialize(cx);
- cx.notify();
- }
-
- fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
- settings::get::<CollaborationPanelSettings>(cx)
- .button
- .then(|| "icons/user_group_16.svg")
- }
-
- fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
- (
- "Collaboration Panel".to_string(),
- Some(Box::new(ToggleFocus)),
- )
- }
-
- fn should_change_position_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::DockPositionChanged)
- }
-
- fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
- self.has_focus
- }
-
- fn is_focus_event(event: &Self::Event) -> bool {
- matches!(event, Event::Focus)
- }
-}
-
-impl PartialEq for ListEntry {
- fn eq(&self, other: &Self) -> bool {
- match self {
- ListEntry::Header(section_1) => {
- if let ListEntry::Header(section_2) = other {
- return section_1 == section_2;
- }
- }
- ListEntry::CallParticipant { user: user_1, .. } => {
- if let ListEntry::CallParticipant { user: user_2, .. } = other {
- return user_1.id == user_2.id;
- }
- }
- ListEntry::ParticipantProject {
- project_id: project_id_1,
- ..
- } => {
- if let ListEntry::ParticipantProject {
- project_id: project_id_2,
- ..
- } = other
- {
- return project_id_1 == project_id_2;
- }
- }
- ListEntry::ParticipantScreen {
- peer_id: peer_id_1, ..
- } => {
- if let ListEntry::ParticipantScreen {
- peer_id: peer_id_2, ..
- } = other
- {
- return peer_id_1 == peer_id_2;
- }
- }
- ListEntry::Channel {
- channel: channel_1, ..
- } => {
- if let ListEntry::Channel {
- channel: channel_2, ..
- } = other
- {
- return channel_1.id == channel_2.id;
- }
- }
- ListEntry::ChannelNotes { channel_id } => {
- if let ListEntry::ChannelNotes {
- channel_id: other_id,
- } = other
- {
- return channel_id == other_id;
- }
- }
- ListEntry::ChannelChat { channel_id } => {
- if let ListEntry::ChannelChat {
- channel_id: other_id,
- } = other
- {
- return channel_id == other_id;
- }
- }
- ListEntry::ChannelInvite(channel_1) => {
- if let ListEntry::ChannelInvite(channel_2) = other {
- return channel_1.id == channel_2.id;
- }
- }
- ListEntry::IncomingRequest(user_1) => {
- if let ListEntry::IncomingRequest(user_2) = other {
- return user_1.id == user_2.id;
- }
- }
- ListEntry::OutgoingRequest(user_1) => {
- if let ListEntry::OutgoingRequest(user_2) = other {
- return user_1.id == user_2.id;
- }
- }
- ListEntry::Contact {
- contact: contact_1, ..
- } => {
- if let ListEntry::Contact {
- contact: contact_2, ..
- } = other
- {
- return contact_1.user.id == contact_2.user.id;
- }
- }
- ListEntry::ChannelEditor { depth } => {
- if let ListEntry::ChannelEditor { depth: other_depth } = other {
- return depth == other_depth;
- }
- }
- ListEntry::ContactPlaceholder => {
- if let ListEntry::ContactPlaceholder = other {
- return true;
- }
- }
- }
- false
- }
-}
-
-fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
- 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)
-}
+// mod channel_modal;
+// mod contact_finder;
+
+// use crate::{
+// channel_view::{self, ChannelView},
+// chat_panel::ChatPanel,
+// face_pile::FacePile,
+// panel_settings, CollaborationPanelSettings,
+// };
+// use anyhow::Result;
+// use call::ActiveCall;
+// use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+// use channel_modal::ChannelModal;
+// use client::{
+// proto::{self, PeerId},
+// Client, Contact, User, UserStore,
+// };
+// use contact_finder::ContactFinder;
+// use context_menu::{ContextMenu, ContextMenuItem};
+// use db::kvp::KEY_VALUE_STORE;
+// use drag_and_drop::{DragAndDrop, Draggable};
+// use editor::{Cancel, Editor};
+// use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
+// use futures::StreamExt;
+// use fuzzy::{match_strings, StringMatchCandidate};
+// use gpui::{
+// actions,
+// elements::{
+// Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
+// ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
+// SafeStylable, Stack, Svg,
+// },
+// fonts::TextStyle,
+// geometry::{
+// rect::RectF,
+// vector::{vec2f, Vector2F},
+// },
+// impl_actions,
+// platform::{CursorStyle, MouseButton, PromptLevel},
+// serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
+// ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+// };
+// use menu::{Confirm, SelectNext, SelectPrev};
+// use project::{Fs, Project};
+// use serde_derive::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
+// use theme::{components::ComponentExt, IconButton, Interactive};
+// use util::{maybe, ResultExt, TryFutureExt};
+// use workspace::{
+// dock::{DockPosition, Panel},
+// item::ItemHandle,
+// FollowNextCollaborator, Workspace,
+// };
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ToggleCollapse {
+// location: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct NewChannel {
+// location: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct RenameChannel {
+// channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ToggleSelectedIx {
+// ix: usize,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct RemoveChannel {
+// channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct InviteMembers {
+// channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct ManageMembers {
+// channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct OpenChannelNotes {
+// pub channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct JoinChannelCall {
+// pub channel_id: u64,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct JoinChannelChat {
+// pub channel_id: u64,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// pub struct CopyChannelLink {
+// pub channel_id: u64,
+// }
+
+// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct StartMoveChannelFor {
+// channel_id: ChannelId,
+// }
+
+// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+// struct MoveChannel {
+// to: ChannelId,
+// }
+
+// actions!(
+// collab_panel,
+// [
+// ToggleFocus,
+// Remove,
+// Secondary,
+// CollapseSelectedChannel,
+// ExpandSelectedChannel,
+// StartMoveChannel,
+// MoveSelected,
+// InsertSpace,
+// ]
+// );
+
+// impl_actions!(
+// collab_panel,
+// [
+// RemoveChannel,
+// NewChannel,
+// InviteMembers,
+// ManageMembers,
+// RenameChannel,
+// ToggleCollapse,
+// OpenChannelNotes,
+// JoinChannelCall,
+// JoinChannelChat,
+// CopyChannelLink,
+// StartMoveChannelFor,
+// MoveChannel,
+// ToggleSelectedIx
+// ]
+// );
+
+// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+// struct ChannelMoveClipboard {
+// channel_id: ChannelId,
+// }
+
+// const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
+
+// pub fn init(cx: &mut AppContext) {
+// settings::register::<panel_settings::CollaborationPanelSettings>(cx);
+// contact_finder::init(cx);
+// channel_modal::init(cx);
+// channel_view::init(cx);
+
+// cx.add_action(CollabPanel::cancel);
+// cx.add_action(CollabPanel::select_next);
+// cx.add_action(CollabPanel::select_prev);
+// cx.add_action(CollabPanel::confirm);
+// cx.add_action(CollabPanel::insert_space);
+// cx.add_action(CollabPanel::remove);
+// cx.add_action(CollabPanel::remove_selected_channel);
+// cx.add_action(CollabPanel::show_inline_context_menu);
+// cx.add_action(CollabPanel::new_subchannel);
+// cx.add_action(CollabPanel::invite_members);
+// cx.add_action(CollabPanel::manage_members);
+// cx.add_action(CollabPanel::rename_selected_channel);
+// cx.add_action(CollabPanel::rename_channel);
+// cx.add_action(CollabPanel::toggle_channel_collapsed_action);
+// cx.add_action(CollabPanel::collapse_selected_channel);
+// cx.add_action(CollabPanel::expand_selected_channel);
+// cx.add_action(CollabPanel::open_channel_notes);
+// cx.add_action(CollabPanel::join_channel_chat);
+// cx.add_action(CollabPanel::copy_channel_link);
+
+// cx.add_action(
+// |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
+// if panel.selection.take() != Some(action.ix) {
+// panel.selection = Some(action.ix)
+// }
+
+// cx.notify();
+// },
+// );
+
+// cx.add_action(
+// |panel: &mut CollabPanel,
+// action: &StartMoveChannelFor,
+// _: &mut ViewContext<CollabPanel>| {
+// panel.channel_clipboard = Some(ChannelMoveClipboard {
+// channel_id: action.channel_id,
+// });
+// },
+// );
+
+// cx.add_action(
+// |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
+// if let Some(channel) = panel.selected_channel() {
+// panel.channel_clipboard = Some(ChannelMoveClipboard {
+// channel_id: channel.id,
+// })
+// }
+// },
+// );
+
+// cx.add_action(
+// |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
+// let Some(clipboard) = panel.channel_clipboard.take() else {
+// return;
+// };
+// let Some(selected_channel) = panel.selected_channel() else {
+// return;
+// };
+
+// panel
+// .channel_store
+// .update(cx, |channel_store, cx| {
+// channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
+// })
+// .detach_and_log_err(cx)
+// },
+// );
+
+// cx.add_action(
+// |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
+// if let Some(clipboard) = panel.channel_clipboard.take() {
+// panel.channel_store.update(cx, |channel_store, cx| {
+// channel_store
+// .move_channel(clipboard.channel_id, Some(action.to), cx)
+// .detach_and_log_err(cx)
+// })
+// }
+// },
+// );
+// }
+
+// #[derive(Debug)]
+// pub enum ChannelEditingState {
+// Create {
+// location: Option<ChannelId>,
+// pending_name: Option<String>,
+// },
+// Rename {
+// location: ChannelId,
+// pending_name: Option<String>,
+// },
+// }
+
+// impl ChannelEditingState {
+// fn pending_name(&self) -> Option<&str> {
+// match self {
+// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+// }
+// }
+// }
+
+// pub struct CollabPanel {
+// width: Option<f32>,
+// fs: Arc<dyn Fs>,
+// has_focus: bool,
+// channel_clipboard: Option<ChannelMoveClipboard>,
+// pending_serialization: Task<Option<()>>,
+// context_menu: ViewHandle<ContextMenu>,
+// filter_editor: ViewHandle<Editor>,
+// channel_name_editor: ViewHandle<Editor>,
+// channel_editing_state: Option<ChannelEditingState>,
+// entries: Vec<ListEntry>,
+// selection: Option<usize>,
+// user_store: ModelHandle<UserStore>,
+// client: Arc<Client>,
+// channel_store: ModelHandle<ChannelStore>,
+// project: ModelHandle<Project>,
+// match_candidates: Vec<StringMatchCandidate>,
+// list_state: ListState<Self>,
+// subscriptions: Vec<Subscription>,
+// collapsed_sections: Vec<Section>,
+// collapsed_channels: Vec<ChannelId>,
+// drag_target_channel: ChannelDragTarget,
+// workspace: WeakViewHandle<Workspace>,
+// context_menu_on_selected: bool,
+// }
+
+// #[derive(PartialEq, Eq)]
+// enum ChannelDragTarget {
+// None,
+// Root,
+// Channel(ChannelId),
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedCollabPanel {
+// width: Option<f32>,
+// collapsed_channels: Option<Vec<ChannelId>>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+// DockPositionChanged,
+// Focus,
+// Dismissed,
+// }
+
+// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+// enum Section {
+// ActiveCall,
+// Channels,
+// ChannelInvites,
+// ContactRequests,
+// Contacts,
+// Online,
+// Offline,
+// }
+
+// #[derive(Clone, Debug)]
+// enum ListEntry {
+// Header(Section),
+// CallParticipant {
+// user: Arc<User>,
+// peer_id: Option<PeerId>,
+// is_pending: bool,
+// },
+// ParticipantProject {
+// project_id: u64,
+// worktree_root_names: Vec<String>,
+// host_user_id: u64,
+// is_last: bool,
+// },
+// ParticipantScreen {
+// peer_id: Option<PeerId>,
+// is_last: bool,
+// },
+// IncomingRequest(Arc<User>),
+// OutgoingRequest(Arc<User>),
+// ChannelInvite(Arc<Channel>),
+// Channel {
+// channel: Arc<Channel>,
+// depth: usize,
+// has_children: bool,
+// },
+// ChannelNotes {
+// channel_id: ChannelId,
+// },
+// ChannelChat {
+// channel_id: ChannelId,
+// },
+// ChannelEditor {
+// depth: usize,
+// },
+// Contact {
+// contact: Arc<Contact>,
+// calling: bool,
+// },
+// ContactPlaceholder,
+// }
+
+// impl Entity for CollabPanel {
+// type Event = Event;
+// }
+
+// impl CollabPanel {
+// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+// cx.add_view::<Self, _>(|cx| {
+// let view_id = cx.view_id();
+
+// let filter_editor = cx.add_view(|cx| {
+// let mut editor = Editor::single_line(
+// Some(Arc::new(|theme| {
+// theme.collab_panel.user_query_editor.clone()
+// })),
+// cx,
+// );
+// editor.set_placeholder_text("Filter channels, contacts", cx);
+// editor
+// });
+
+// cx.subscribe(&filter_editor, |this, _, event, cx| {
+// if let editor::Event::BufferEdited = event {
+// let query = this.filter_editor.read(cx).text(cx);
+// if !query.is_empty() {
+// this.selection.take();
+// }
+// this.update_entries(true, cx);
+// if !query.is_empty() {
+// this.selection = this
+// .entries
+// .iter()
+// .position(|entry| !matches!(entry, ListEntry::Header(_)));
+// }
+// } else if let editor::Event::Blurred = event {
+// let query = this.filter_editor.read(cx).text(cx);
+// if query.is_empty() {
+// this.selection.take();
+// this.update_entries(true, cx);
+// }
+// }
+// })
+// .detach();
+
+// let channel_name_editor = cx.add_view(|cx| {
+// Editor::single_line(
+// Some(Arc::new(|theme| {
+// theme.collab_panel.user_query_editor.clone()
+// })),
+// cx,
+// )
+// });
+
+// cx.subscribe(&channel_name_editor, |this, _, event, cx| {
+// if let editor::Event::Blurred = event {
+// if let Some(state) = &this.channel_editing_state {
+// if state.pending_name().is_some() {
+// return;
+// }
+// }
+// this.take_editing_state(cx);
+// this.update_entries(false, cx);
+// cx.notify();
+// }
+// })
+// .detach();
+
+// let list_state =
+// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+// let theme = theme::current(cx).clone();
+// let is_selected = this.selection == Some(ix);
+// let current_project_id = this.project.read(cx).remote_id();
+
+// match &this.entries[ix] {
+// ListEntry::Header(section) => {
+// let is_collapsed = this.collapsed_sections.contains(section);
+// this.render_header(*section, &theme, is_selected, is_collapsed, cx)
+// }
+// ListEntry::CallParticipant {
+// user,
+// peer_id,
+// is_pending,
+// } => Self::render_call_participant(
+// user,
+// *peer_id,
+// this.user_store.clone(),
+// *is_pending,
+// is_selected,
+// &theme,
+// cx,
+// ),
+// ListEntry::ParticipantProject {
+// project_id,
+// worktree_root_names,
+// host_user_id,
+// is_last,
+// } => Self::render_participant_project(
+// *project_id,
+// worktree_root_names,
+// *host_user_id,
+// Some(*project_id) == current_project_id,
+// *is_last,
+// is_selected,
+// &theme,
+// cx,
+// ),
+// ListEntry::ParticipantScreen { peer_id, is_last } => {
+// Self::render_participant_screen(
+// *peer_id,
+// *is_last,
+// is_selected,
+// &theme.collab_panel,
+// cx,
+// )
+// }
+// ListEntry::Channel {
+// channel,
+// depth,
+// has_children,
+// } => {
+// let channel_row = this.render_channel(
+// &*channel,
+// *depth,
+// &theme,
+// is_selected,
+// *has_children,
+// ix,
+// cx,
+// );
+
+// if is_selected && this.context_menu_on_selected {
+// Stack::new()
+// .with_child(channel_row)
+// .with_child(
+// ChildView::new(&this.context_menu, cx)
+// .aligned()
+// .bottom()
+// .right(),
+// )
+// .into_any()
+// } else {
+// return channel_row;
+// }
+// }
+// ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
+// *channel_id,
+// &theme.collab_panel,
+// is_selected,
+// ix,
+// cx,
+// ),
+// ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
+// *channel_id,
+// &theme.collab_panel,
+// is_selected,
+// ix,
+// cx,
+// ),
+// ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
+// channel.clone(),
+// this.channel_store.clone(),
+// &theme.collab_panel,
+// is_selected,
+// cx,
+// ),
+// ListEntry::IncomingRequest(user) => Self::render_contact_request(
+// user.clone(),
+// this.user_store.clone(),
+// &theme.collab_panel,
+// true,
+// is_selected,
+// cx,
+// ),
+// ListEntry::OutgoingRequest(user) => Self::render_contact_request(
+// user.clone(),
+// this.user_store.clone(),
+// &theme.collab_panel,
+// false,
+// is_selected,
+// cx,
+// ),
+// ListEntry::Contact { contact, calling } => Self::render_contact(
+// contact,
+// *calling,
+// &this.project,
+// &theme,
+// is_selected,
+// cx,
+// ),
+// ListEntry::ChannelEditor { depth } => {
+// this.render_channel_editor(&theme, *depth, cx)
+// }
+// ListEntry::ContactPlaceholder => {
+// this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
+// }
+// }
+// });
+
+// let mut this = Self {
+// width: None,
+// has_focus: false,
+// channel_clipboard: None,
+// fs: workspace.app_state().fs.clone(),
+// pending_serialization: Task::ready(None),
+// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+// channel_name_editor,
+// filter_editor,
+// entries: Vec::default(),
+// channel_editing_state: None,
+// selection: None,
+// user_store: workspace.user_store().clone(),
+// channel_store: ChannelStore::global(cx),
+// project: workspace.project().clone(),
+// subscriptions: Vec::default(),
+// match_candidates: Vec::default(),
+// collapsed_sections: vec![Section::Offline],
+// collapsed_channels: Vec::default(),
+// workspace: workspace.weak_handle(),
+// client: workspace.app_state().client.clone(),
+// context_menu_on_selected: true,
+// drag_target_channel: ChannelDragTarget::None,
+// list_state,
+// };
+
+// this.update_entries(false, cx);
+
+// // Update the dock position when the setting changes.
+// 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();
+// }),
+// );
+
+// let active_call = ActiveCall::global(cx);
+// this.subscriptions
+// .push(cx.observe(&this.user_store, |this, _, cx| {
+// this.update_entries(true, cx)
+// }));
+// this.subscriptions
+// .push(cx.observe(&this.channel_store, |this, _, cx| {
+// this.update_entries(true, cx)
+// }));
+// this.subscriptions
+// .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
+// this.subscriptions
+// .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
+// this.update_entries(true, cx)
+// }));
+// this.subscriptions.push(cx.subscribe(
+// &this.channel_store,
+// |this, _channel_store, e, cx| match e {
+// ChannelEvent::ChannelCreated(channel_id)
+// | ChannelEvent::ChannelRenamed(channel_id) => {
+// if this.take_editing_state(cx) {
+// this.update_entries(false, cx);
+// this.selection = this.entries.iter().position(|entry| {
+// if let ListEntry::Channel { channel, .. } = entry {
+// channel.id == *channel_id
+// } else {
+// false
+// }
+// });
+// }
+// }
+// },
+// ));
+
+// this
+// })
+// }
+
+// pub fn load(
+// workspace: WeakViewHandle<Workspace>,
+// cx: AsyncAppContext,
+// ) -> Task<Result<ViewHandle<Self>>> {
+// cx.spawn(|mut cx| async move {
+// let serialized_panel = if let Some(panel) = cx
+// .background()
+// .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
+// .await
+// .log_err()
+// .flatten()
+// {
+// match serde_json::from_str::<SerializedCollabPanel>(&panel) {
+// Ok(panel) => Some(panel),
+// Err(err) => {
+// log::error!("Failed to deserialize collaboration panel: {}", err);
+// None
+// }
+// }
+// } else {
+// None
+// };
+
+// workspace.update(&mut cx, |workspace, cx| {
+// let panel = CollabPanel::new(workspace, cx);
+// if let Some(serialized_panel) = serialized_panel {
+// panel.update(cx, |panel, cx| {
+// panel.width = serialized_panel.width;
+// panel.collapsed_channels = serialized_panel
+// .collapsed_channels
+// .unwrap_or_else(|| Vec::new());
+// cx.notify();
+// });
+// }
+// panel
+// })
+// })
+// }
+
+// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+// let width = self.width;
+// let collapsed_channels = self.collapsed_channels.clone();
+// self.pending_serialization = cx.background().spawn(
+// async move {
+// KEY_VALUE_STORE
+// .write_kvp(
+// COLLABORATION_PANEL_KEY.into(),
+// serde_json::to_string(&SerializedCollabPanel {
+// width,
+// collapsed_channels: Some(collapsed_channels),
+// })?,
+// )
+// .await?;
+// anyhow::Ok(())
+// }
+// .log_err(),
+// );
+// }
+
+// fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+// let channel_store = self.channel_store.read(cx);
+// let user_store = self.user_store.read(cx);
+// let query = self.filter_editor.read(cx).text(cx);
+// let executor = cx.background().clone();
+
+// let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+// let old_entries = mem::take(&mut self.entries);
+// let mut scroll_to_top = false;
+
+// if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+// self.entries.push(ListEntry::Header(Section::ActiveCall));
+// if !old_entries
+// .iter()
+// .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+// {
+// scroll_to_top = true;
+// }
+
+// if !self.collapsed_sections.contains(&Section::ActiveCall) {
+// let room = room.read(cx);
+
+// if let Some(channel_id) = room.channel_id() {
+// self.entries.push(ListEntry::ChannelNotes { channel_id });
+// self.entries.push(ListEntry::ChannelChat { channel_id })
+// }
+
+// // Populate the active user.
+// if let Some(user) = user_store.current_user() {
+// self.match_candidates.clear();
+// self.match_candidates.push(StringMatchCandidate {
+// id: 0,
+// string: user.github_login.clone(),
+// char_bag: user.github_login.chars().collect(),
+// });
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// if !matches.is_empty() {
+// let user_id = user.id;
+// self.entries.push(ListEntry::CallParticipant {
+// user,
+// peer_id: None,
+// is_pending: false,
+// });
+// let mut projects = room.local_participant().projects.iter().peekable();
+// while let Some(project) = projects.next() {
+// self.entries.push(ListEntry::ParticipantProject {
+// project_id: project.id,
+// worktree_root_names: project.worktree_root_names.clone(),
+// host_user_id: user_id,
+// is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+// });
+// }
+// if room.is_screen_sharing() {
+// self.entries.push(ListEntry::ParticipantScreen {
+// peer_id: None,
+// is_last: true,
+// });
+// }
+// }
+// }
+
+// // Populate remote participants.
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(room.remote_participants().iter().map(|(_, participant)| {
+// StringMatchCandidate {
+// id: participant.user.id as usize,
+// string: participant.user.github_login.clone(),
+// char_bag: participant.user.github_login.chars().collect(),
+// }
+// }));
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// for mat in matches {
+// let user_id = mat.candidate_id as u64;
+// let participant = &room.remote_participants()[&user_id];
+// self.entries.push(ListEntry::CallParticipant {
+// user: participant.user.clone(),
+// peer_id: Some(participant.peer_id),
+// is_pending: false,
+// });
+// let mut projects = participant.projects.iter().peekable();
+// while let Some(project) = projects.next() {
+// self.entries.push(ListEntry::ParticipantProject {
+// project_id: project.id,
+// worktree_root_names: project.worktree_root_names.clone(),
+// host_user_id: participant.user.id,
+// is_last: projects.peek().is_none()
+// && participant.video_tracks.is_empty(),
+// });
+// }
+// if !participant.video_tracks.is_empty() {
+// self.entries.push(ListEntry::ParticipantScreen {
+// peer_id: Some(participant.peer_id),
+// is_last: true,
+// });
+// }
+// }
+
+// // Populate pending participants.
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(room.pending_participants().iter().enumerate().map(
+// |(id, participant)| StringMatchCandidate {
+// id,
+// string: participant.github_login.clone(),
+// char_bag: participant.github_login.chars().collect(),
+// },
+// ));
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// self.entries
+// .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+// user: room.pending_participants()[mat.candidate_id].clone(),
+// peer_id: None,
+// is_pending: true,
+// }));
+// }
+// }
+
+// let mut request_entries = Vec::new();
+
+// if cx.has_flag::<ChannelsAlpha>() {
+// self.entries.push(ListEntry::Header(Section::Channels));
+
+// if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(channel_store.ordered_channels().enumerate().map(
+// |(ix, (_, channel))| StringMatchCandidate {
+// id: ix,
+// string: channel.name.clone(),
+// char_bag: channel.name.chars().collect(),
+// },
+// ));
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// if let Some(state) = &self.channel_editing_state {
+// if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+// self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+// }
+// }
+// let mut collapse_depth = None;
+// for mat in matches {
+// let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+// let depth = channel.parent_path.len();
+
+// if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+// collapse_depth = Some(depth);
+// } else if let Some(collapsed_depth) = collapse_depth {
+// if depth > collapsed_depth {
+// continue;
+// }
+// if self.is_channel_collapsed(channel.id) {
+// collapse_depth = Some(depth);
+// } else {
+// collapse_depth = None;
+// }
+// }
+
+// let has_children = channel_store
+// .channel_at_index(mat.candidate_id + 1)
+// .map_or(false, |next_channel| {
+// next_channel.parent_path.ends_with(&[channel.id])
+// });
+
+// match &self.channel_editing_state {
+// Some(ChannelEditingState::Create {
+// location: parent_id,
+// ..
+// }) if *parent_id == Some(channel.id) => {
+// self.entries.push(ListEntry::Channel {
+// channel: channel.clone(),
+// depth,
+// has_children: false,
+// });
+// self.entries
+// .push(ListEntry::ChannelEditor { depth: depth + 1 });
+// }
+// Some(ChannelEditingState::Rename {
+// location: parent_id,
+// ..
+// }) if parent_id == &channel.id => {
+// self.entries.push(ListEntry::ChannelEditor { depth });
+// }
+// _ => {
+// self.entries.push(ListEntry::Channel {
+// channel: channel.clone(),
+// depth,
+// has_children,
+// });
+// }
+// }
+// }
+// }
+
+// let channel_invites = channel_store.channel_invitations();
+// if !channel_invites.is_empty() {
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+// StringMatchCandidate {
+// id: ix,
+// string: channel.name.clone(),
+// char_bag: channel.name.chars().collect(),
+// }
+// }));
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// request_entries.extend(matches.iter().map(|mat| {
+// ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+// }));
+
+// if !request_entries.is_empty() {
+// self.entries
+// .push(ListEntry::Header(Section::ChannelInvites));
+// if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+// self.entries.append(&mut request_entries);
+// }
+// }
+// }
+// }
+
+// self.entries.push(ListEntry::Header(Section::Contacts));
+
+// request_entries.clear();
+// let incoming = user_store.incoming_contact_requests();
+// if !incoming.is_empty() {
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(
+// incoming
+// .iter()
+// .enumerate()
+// .map(|(ix, user)| StringMatchCandidate {
+// id: ix,
+// string: user.github_login.clone(),
+// char_bag: user.github_login.chars().collect(),
+// }),
+// );
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// request_entries.extend(
+// matches
+// .iter()
+// .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+// );
+// }
+
+// let outgoing = user_store.outgoing_contact_requests();
+// if !outgoing.is_empty() {
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(
+// outgoing
+// .iter()
+// .enumerate()
+// .map(|(ix, user)| StringMatchCandidate {
+// id: ix,
+// string: user.github_login.clone(),
+// char_bag: user.github_login.chars().collect(),
+// }),
+// );
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+// request_entries.extend(
+// matches
+// .iter()
+// .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+// );
+// }
+
+// if !request_entries.is_empty() {
+// self.entries
+// .push(ListEntry::Header(Section::ContactRequests));
+// if !self.collapsed_sections.contains(&Section::ContactRequests) {
+// self.entries.append(&mut request_entries);
+// }
+// }
+
+// let contacts = user_store.contacts();
+// if !contacts.is_empty() {
+// self.match_candidates.clear();
+// self.match_candidates
+// .extend(
+// contacts
+// .iter()
+// .enumerate()
+// .map(|(ix, contact)| StringMatchCandidate {
+// id: ix,
+// string: contact.user.github_login.clone(),
+// char_bag: contact.user.github_login.chars().collect(),
+// }),
+// );
+
+// let matches = executor.block(match_strings(
+// &self.match_candidates,
+// &query,
+// true,
+// usize::MAX,
+// &Default::default(),
+// executor.clone(),
+// ));
+
+// let (online_contacts, offline_contacts) = matches
+// .iter()
+// .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+// for (matches, section) in [
+// (online_contacts, Section::Online),
+// (offline_contacts, Section::Offline),
+// ] {
+// if !matches.is_empty() {
+// self.entries.push(ListEntry::Header(section));
+// if !self.collapsed_sections.contains(§ion) {
+// let active_call = &ActiveCall::global(cx).read(cx);
+// for mat in matches {
+// let contact = &contacts[mat.candidate_id];
+// self.entries.push(ListEntry::Contact {
+// contact: contact.clone(),
+// calling: active_call.pending_invites().contains(&contact.user.id),
+// });
+// }
+// }
+// }
+// }
+// }
+
+// if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+// self.entries.push(ListEntry::ContactPlaceholder);
+// }
+
+// if select_same_item {
+// if let Some(prev_selected_entry) = prev_selected_entry {
+// self.selection.take();
+// for (ix, entry) in self.entries.iter().enumerate() {
+// if *entry == prev_selected_entry {
+// self.selection = Some(ix);
+// break;
+// }
+// }
+// }
+// } else {
+// self.selection = self.selection.and_then(|prev_selection| {
+// if self.entries.is_empty() {
+// None
+// } else {
+// Some(prev_selection.min(self.entries.len() - 1))
+// }
+// });
+// }
+
+// let old_scroll_top = self.list_state.logical_scroll_top();
+
+// self.list_state.reset(self.entries.len());
+
+// if scroll_to_top {
+// self.list_state.scroll_to(ListOffset::default());
+// } else {
+// // Attempt to maintain the same scroll position.
+// if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+// let new_scroll_top = self
+// .entries
+// .iter()
+// .position(|entry| entry == old_top_entry)
+// .map(|item_ix| ListOffset {
+// item_ix,
+// offset_in_item: old_scroll_top.offset_in_item,
+// })
+// .or_else(|| {
+// let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+// let item_ix = self
+// .entries
+// .iter()
+// .position(|entry| entry == entry_after_old_top)?;
+// Some(ListOffset {
+// item_ix,
+// offset_in_item: 0.,
+// })
+// })
+// .or_else(|| {
+// let entry_before_old_top =
+// old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+// let item_ix = self
+// .entries
+// .iter()
+// .position(|entry| entry == entry_before_old_top)?;
+// Some(ListOffset {
+// item_ix,
+// offset_in_item: 0.,
+// })
+// });
+
+// self.list_state
+// .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+// }
+// }
+
+// cx.notify();
+// }
+
+// fn render_call_participant(
+// user: &User,
+// peer_id: Option<PeerId>,
+// user_store: ModelHandle<UserStore>,
+// is_pending: bool,
+// is_selected: bool,
+// theme: &theme::Theme,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum CallParticipant {}
+// enum CallParticipantTooltip {}
+// enum LeaveCallButton {}
+// enum LeaveCallTooltip {}
+
+// let collab_theme = &theme.collab_panel;
+
+// let is_current_user =
+// user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+
+// let content = MouseEventHandler::new::<CallParticipant, _>(
+// user.id as usize,
+// cx,
+// |mouse_state, cx| {
+// let style = if is_current_user {
+// *collab_theme
+// .contact_row
+// .in_state(is_selected)
+// .style_for(&mut Default::default())
+// } else {
+// *collab_theme
+// .contact_row
+// .in_state(is_selected)
+// .style_for(mouse_state)
+// };
+
+// Flex::row()
+// .with_children(user.avatar.clone().map(|avatar| {
+// Image::from_data(avatar)
+// .with_style(collab_theme.contact_avatar)
+// .aligned()
+// .left()
+// }))
+// .with_child(
+// Label::new(
+// user.github_login.clone(),
+// collab_theme.contact_username.text.clone(),
+// )
+// .contained()
+// .with_style(collab_theme.contact_username.container)
+// .aligned()
+// .left()
+// .flex(1., true),
+// )
+// .with_children(if is_pending {
+// Some(
+// Label::new("Calling", collab_theme.calling_indicator.text.clone())
+// .contained()
+// .with_style(collab_theme.calling_indicator.container)
+// .aligned()
+// .into_any(),
+// )
+// } else if is_current_user {
+// Some(
+// MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
+// render_icon_button(
+// theme
+// .collab_panel
+// .leave_call_button
+// .style_for(is_selected, state),
+// "icons/exit.svg",
+// )
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, |_, _, cx| {
+// Self::leave_call(cx);
+// })
+// .with_tooltip::<LeaveCallTooltip>(
+// 0,
+// "Leave call",
+// None,
+// theme.tooltip.clone(),
+// cx,
+// )
+// .into_any(),
+// )
+// } else {
+// None
+// })
+// .constrained()
+// .with_height(collab_theme.row_height)
+// .contained()
+// .with_style(style)
+// },
+// );
+
+// if is_current_user || is_pending || peer_id.is_none() {
+// return content.into_any();
+// }
+
+// let tooltip = format!("Follow {}", user.github_login);
+
+// content
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// if let Some(workspace) = this.workspace.upgrade(cx) {
+// workspace
+// .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
+// .map(|task| task.detach_and_log_err(cx));
+// }
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .with_tooltip::<CallParticipantTooltip>(
+// user.id as usize,
+// tooltip,
+// Some(Box::new(FollowNextCollaborator)),
+// theme.tooltip.clone(),
+// cx,
+// )
+// .into_any()
+// }
+
+// fn render_participant_project(
+// project_id: u64,
+// worktree_root_names: &[String],
+// host_user_id: u64,
+// is_current: bool,
+// is_last: bool,
+// is_selected: bool,
+// theme: &theme::Theme,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum JoinProject {}
+// enum JoinProjectTooltip {}
+
+// let collab_theme = &theme.collab_panel;
+// let host_avatar_width = collab_theme
+// .contact_avatar
+// .width
+// .or(collab_theme.contact_avatar.height)
+// .unwrap_or(0.);
+// let tree_branch = collab_theme.tree_branch;
+// let project_name = if worktree_root_names.is_empty() {
+// "untitled".to_string()
+// } else {
+// worktree_root_names.join(", ")
+// };
+
+// let content =
+// MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+// let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+// let row = if is_current {
+// collab_theme
+// .project_row
+// .in_state(true)
+// .style_for(&mut Default::default())
+// } else {
+// collab_theme
+// .project_row
+// .in_state(is_selected)
+// .style_for(mouse_state)
+// };
+
+// Flex::row()
+// .with_child(render_tree_branch(
+// tree_branch,
+// &row.name.text,
+// is_last,
+// vec2f(host_avatar_width, collab_theme.row_height),
+// cx.font_cache(),
+// ))
+// .with_child(
+// Svg::new("icons/file_icons/folder.svg")
+// .with_color(collab_theme.channel_hash.color)
+// .constrained()
+// .with_width(collab_theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new(project_name.clone(), row.name.text.clone())
+// .aligned()
+// .left()
+// .contained()
+// .with_style(row.name.container)
+// .flex(1., false),
+// )
+// .constrained()
+// .with_height(collab_theme.row_height)
+// .contained()
+// .with_style(row.container)
+// });
+
+// if is_current {
+// return content.into_any();
+// }
+
+// content
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// if let Some(workspace) = this.workspace.upgrade(cx) {
+// let app_state = workspace.read(cx).app_state().clone();
+// workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+// .detach_and_log_err(cx);
+// }
+// })
+// .with_tooltip::<JoinProjectTooltip>(
+// project_id as usize,
+// format!("Open {}", project_name),
+// None,
+// theme.tooltip.clone(),
+// cx,
+// )
+// .into_any()
+// }
+
+// fn render_participant_screen(
+// peer_id: Option<PeerId>,
+// is_last: bool,
+// is_selected: bool,
+// theme: &theme::CollabPanel,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum OpenSharedScreen {}
+
+// let host_avatar_width = theme
+// .contact_avatar
+// .width
+// .or(theme.contact_avatar.height)
+// .unwrap_or(0.);
+// let tree_branch = theme.tree_branch;
+
+// let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
+// peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
+// cx,
+// |mouse_state, cx| {
+// let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+// let row = theme
+// .project_row
+// .in_state(is_selected)
+// .style_for(mouse_state);
+
+// Flex::row()
+// .with_child(render_tree_branch(
+// tree_branch,
+// &row.name.text,
+// is_last,
+// vec2f(host_avatar_width, theme.row_height),
+// cx.font_cache(),
+// ))
+// .with_child(
+// Svg::new("icons/desktop.svg")
+// .with_color(theme.channel_hash.color)
+// .constrained()
+// .with_width(theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new("Screen", row.name.text.clone())
+// .aligned()
+// .left()
+// .contained()
+// .with_style(row.name.container)
+// .flex(1., false),
+// )
+// .constrained()
+// .with_height(theme.row_height)
+// .contained()
+// .with_style(row.container)
+// },
+// );
+// if peer_id.is_none() {
+// return handler.into_any();
+// }
+// handler
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// if let Some(workspace) = this.workspace.upgrade(cx) {
+// workspace.update(cx, |workspace, cx| {
+// workspace.open_shared_screen(peer_id.unwrap(), cx)
+// });
+// }
+// })
+// .into_any()
+// }
+
+// fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
+// if let Some(_) = self.channel_editing_state.take() {
+// self.channel_name_editor.update(cx, |editor, cx| {
+// editor.set_text("", cx);
+// });
+// true
+// } else {
+// false
+// }
+// }
+
+// fn render_header(
+// &self,
+// section: Section,
+// theme: &theme::Theme,
+// is_selected: bool,
+// is_collapsed: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum Header {}
+// enum LeaveCallContactList {}
+// enum AddChannel {}
+
+// let tooltip_style = &theme.tooltip;
+// let mut channel_link = None;
+// let mut channel_tooltip_text = None;
+// let mut channel_icon = None;
+// let mut is_dragged_over = false;
+
+// let text = match section {
+// Section::ActiveCall => {
+// let channel_name = maybe!({
+// let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
+
+// let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
+
+// channel_link = Some(channel.link());
+// (channel_icon, channel_tooltip_text) = match channel.visibility {
+// proto::ChannelVisibility::Public => {
+// (Some("icons/public.svg"), Some("Copy public channel link."))
+// }
+// proto::ChannelVisibility::Members => {
+// (Some("icons/hash.svg"), Some("Copy private channel link."))
+// }
+// };
+
+// Some(channel.name.as_str())
+// });
+
+// if let Some(name) = channel_name {
+// Cow::Owned(format!("{}", name))
+// } else {
+// Cow::Borrowed("Current Call")
+// }
+// }
+// Section::ContactRequests => Cow::Borrowed("Requests"),
+// Section::Contacts => Cow::Borrowed("Contacts"),
+// Section::Channels => Cow::Borrowed("Channels"),
+// Section::ChannelInvites => Cow::Borrowed("Invites"),
+// Section::Online => Cow::Borrowed("Online"),
+// Section::Offline => Cow::Borrowed("Offline"),
+// };
+
+// enum AddContact {}
+// let button = match section {
+// Section::ActiveCall => channel_link.map(|channel_link| {
+// let channel_link_copy = channel_link.clone();
+// MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
+// render_icon_button(
+// theme
+// .collab_panel
+// .leave_call_button
+// .style_for(is_selected, state),
+// "icons/link.svg",
+// )
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, _, cx| {
+// let item = ClipboardItem::new(channel_link_copy.clone());
+// cx.write_to_clipboard(item)
+// })
+// .with_tooltip::<AddContact>(
+// 0,
+// channel_tooltip_text.unwrap(),
+// None,
+// tooltip_style.clone(),
+// cx,
+// )
+// }),
+// Section::Contacts => Some(
+// MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
+// render_icon_button(
+// theme
+// .collab_panel
+// .add_contact_button
+// .style_for(is_selected, state),
+// "icons/plus.svg",
+// )
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, |_, this, cx| {
+// this.toggle_contact_finder(cx);
+// })
+// .with_tooltip::<LeaveCallContactList>(
+// 0,
+// "Search for new contact",
+// None,
+// tooltip_style.clone(),
+// cx,
+// ),
+// ),
+// Section::Channels => {
+// if cx
+// .global::<DragAndDrop<Workspace>>()
+// .currently_dragged::<Channel>(cx.window())
+// .is_some()
+// && self.drag_target_channel == ChannelDragTarget::Root
+// {
+// is_dragged_over = true;
+// }
+
+// Some(
+// MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
+// render_icon_button(
+// theme
+// .collab_panel
+// .add_contact_button
+// .style_for(is_selected, state),
+// "icons/plus.svg",
+// )
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
+// .with_tooltip::<AddChannel>(
+// 0,
+// "Create a channel",
+// None,
+// tooltip_style.clone(),
+// cx,
+// ),
+// )
+// }
+// _ => None,
+// };
+
+// let can_collapse = match section {
+// Section::ActiveCall | Section::Channels | Section::Contacts => false,
+// Section::ChannelInvites
+// | Section::ContactRequests
+// | Section::Online
+// | Section::Offline => true,
+// };
+// let icon_size = (&theme.collab_panel).section_icon_size;
+// let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
+// let header_style = if can_collapse {
+// theme
+// .collab_panel
+// .subheader_row
+// .in_state(is_selected)
+// .style_for(state)
+// } else {
+// &theme.collab_panel.header_row
+// };
+
+// Flex::row()
+// .with_children(if can_collapse {
+// Some(
+// Svg::new(if is_collapsed {
+// "icons/chevron_right.svg"
+// } else {
+// "icons/chevron_down.svg"
+// })
+// .with_color(header_style.text.color)
+// .constrained()
+// .with_max_width(icon_size)
+// .with_max_height(icon_size)
+// .aligned()
+// .constrained()
+// .with_width(icon_size)
+// .contained()
+// .with_margin_right(
+// theme.collab_panel.contact_username.container.margin.left,
+// ),
+// )
+// } else if let Some(channel_icon) = channel_icon {
+// Some(
+// Svg::new(channel_icon)
+// .with_color(header_style.text.color)
+// .constrained()
+// .with_max_width(icon_size)
+// .with_max_height(icon_size)
+// .aligned()
+// .constrained()
+// .with_width(icon_size)
+// .contained()
+// .with_margin_right(
+// theme.collab_panel.contact_username.container.margin.left,
+// ),
+// )
+// } else {
+// None
+// })
+// .with_child(
+// Label::new(text, header_style.text.clone())
+// .aligned()
+// .left()
+// .flex(1., true),
+// )
+// .with_children(button.map(|button| button.aligned().right()))
+// .constrained()
+// .with_height(theme.collab_panel.row_height)
+// .contained()
+// .with_style(if is_dragged_over {
+// theme.collab_panel.dragged_over_header
+// } else {
+// header_style.container
+// })
+// });
+
+// result = result
+// .on_move(move |_, this, cx| {
+// if cx
+// .global::<DragAndDrop<Workspace>>()
+// .currently_dragged::<Channel>(cx.window())
+// .is_some()
+// {
+// this.drag_target_channel = ChannelDragTarget::Root;
+// cx.notify()
+// }
+// })
+// .on_up(MouseButton::Left, move |_, this, cx| {
+// if let Some((_, dragged_channel)) = cx
+// .global::<DragAndDrop<Workspace>>()
+// .currently_dragged::<Channel>(cx.window())
+// {
+// this.channel_store
+// .update(cx, |channel_store, cx| {
+// channel_store.move_channel(dragged_channel.id, None, cx)
+// })
+// .detach_and_log_err(cx)
+// }
+// });
+
+// if can_collapse {
+// result = result
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// if can_collapse {
+// this.toggle_section_expanded(section, cx);
+// }
+// })
+// }
+
+// result.into_any()
+// }
+
+// fn render_contact(
+// contact: &Contact,
+// calling: bool,
+// project: &ModelHandle<Project>,
+// theme: &theme::Theme,
+// is_selected: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum ContactTooltip {}
+
+// let collab_theme = &theme.collab_panel;
+// let online = contact.online;
+// let busy = contact.busy || calling;
+// let user_id = contact.user.id;
+// let github_login = contact.user.github_login.clone();
+// let initial_project = project.clone();
+
+// let event_handler =
+// MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
+// Flex::row()
+// .with_children(contact.user.avatar.clone().map(|avatar| {
+// let status_badge = if contact.online {
+// Some(
+// Empty::new()
+// .collapsed()
+// .contained()
+// .with_style(if busy {
+// collab_theme.contact_status_busy
+// } else {
+// collab_theme.contact_status_free
+// })
+// .aligned(),
+// )
+// } else {
+// None
+// };
+// Stack::new()
+// .with_child(
+// Image::from_data(avatar)
+// .with_style(collab_theme.contact_avatar)
+// .aligned()
+// .left(),
+// )
+// .with_children(status_badge)
+// }))
+// .with_child(
+// Label::new(
+// contact.user.github_login.clone(),
+// collab_theme.contact_username.text.clone(),
+// )
+// .contained()
+// .with_style(collab_theme.contact_username.container)
+// .aligned()
+// .left()
+// .flex(1., true),
+// )
+// .with_children(if state.hovered() {
+// Some(
+// MouseEventHandler::new::<Cancel, _>(
+// contact.user.id as usize,
+// cx,
+// |mouse_state, _| {
+// let button_style =
+// collab_theme.contact_button.style_for(mouse_state);
+// render_icon_button(button_style, "icons/x.svg")
+// .aligned()
+// .flex_float()
+// },
+// )
+// .with_padding(Padding::uniform(2.))
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.remove_contact(user_id, &github_login, cx);
+// })
+// .flex_float(),
+// )
+// } else {
+// None
+// })
+// .with_children(if calling {
+// Some(
+// Label::new("Calling", collab_theme.calling_indicator.text.clone())
+// .contained()
+// .with_style(collab_theme.calling_indicator.container)
+// .aligned(),
+// )
+// } else {
+// None
+// })
+// .constrained()
+// .with_height(collab_theme.row_height)
+// .contained()
+// .with_style(
+// *collab_theme
+// .contact_row
+// .in_state(is_selected)
+// .style_for(state),
+// )
+// });
+
+// if online && !busy {
+// let room = ActiveCall::global(cx).read(cx).room();
+// let label = if room.is_some() {
+// format!("Invite {} to join call", contact.user.github_login)
+// } else {
+// format!("Call {}", contact.user.github_login)
+// };
+
+// event_handler
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.call(user_id, Some(initial_project.clone()), cx);
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .with_tooltip::<ContactTooltip>(
+// contact.user.id as usize,
+// label,
+// None,
+// theme.tooltip.clone(),
+// cx,
+// )
+// .into_any()
+// } else {
+// event_handler
+// .with_tooltip::<ContactTooltip>(
+// contact.user.id as usize,
+// format!(
+// "{} is {}",
+// contact.user.github_login,
+// if busy { "on a call" } else { "offline" }
+// ),
+// None,
+// theme.tooltip.clone(),
+// cx,
+// )
+// .into_any()
+// }
+// }
+
+// fn render_contact_placeholder(
+// &self,
+// theme: &theme::CollabPanel,
+// is_selected: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum AddContacts {}
+// MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
+// let style = theme.list_empty_state.style_for(is_selected, state);
+// Flex::row()
+// .with_child(
+// Svg::new("icons/plus.svg")
+// .with_color(theme.list_empty_icon.color)
+// .constrained()
+// .with_width(theme.list_empty_icon.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new("Add a contact", style.text.clone())
+// .contained()
+// .with_style(theme.list_empty_label_container),
+// )
+// .align_children_center()
+// .contained()
+// .with_style(style.container)
+// .into_any()
+// })
+// .on_click(MouseButton::Left, |_, this, cx| {
+// this.toggle_contact_finder(cx);
+// })
+// .into_any()
+// }
+
+// fn render_channel_editor(
+// &self,
+// theme: &theme::Theme,
+// depth: usize,
+// cx: &AppContext,
+// ) -> AnyElement<Self> {
+// Flex::row()
+// .with_child(
+// Empty::new()
+// .constrained()
+// .with_width(theme.collab_panel.disclosure.button_space()),
+// )
+// .with_child(
+// Svg::new("icons/hash.svg")
+// .with_color(theme.collab_panel.channel_hash.color)
+// .constrained()
+// .with_width(theme.collab_panel.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// if let Some(pending_name) = self
+// .channel_editing_state
+// .as_ref()
+// .and_then(|state| state.pending_name())
+// {
+// Label::new(
+// pending_name.to_string(),
+// theme.collab_panel.contact_username.text.clone(),
+// )
+// .contained()
+// .with_style(theme.collab_panel.contact_username.container)
+// .aligned()
+// .left()
+// .flex(1., true)
+// .into_any()
+// } else {
+// ChildView::new(&self.channel_name_editor, cx)
+// .aligned()
+// .left()
+// .contained()
+// .with_style(theme.collab_panel.channel_editor)
+// .flex(1.0, true)
+// .into_any()
+// },
+// )
+// .align_children_center()
+// .constrained()
+// .with_height(theme.collab_panel.row_height)
+// .contained()
+// .with_style(ContainerStyle {
+// background_color: Some(theme.editor.background),
+// ..*theme.collab_panel.contact_row.default_style()
+// })
+// .with_padding_left(
+// theme.collab_panel.contact_row.default_style().padding.left
+// + theme.collab_panel.channel_indent * depth as f32,
+// )
+// .into_any()
+// }
+
+// fn render_channel(
+// &self,
+// channel: &Channel,
+// depth: usize,
+// theme: &theme::Theme,
+// is_selected: bool,
+// has_children: bool,
+// ix: usize,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// let channel_id = channel.id;
+// let collab_theme = &theme.collab_panel;
+// let is_public = self
+// .channel_store
+// .read(cx)
+// .channel_for_id(channel_id)
+// .map(|channel| channel.visibility)
+// == Some(proto::ChannelVisibility::Public);
+// let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
+// let disclosed =
+// has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
+
+// let is_active = maybe!({
+// let call_channel = ActiveCall::global(cx)
+// .read(cx)
+// .room()?
+// .read(cx)
+// .channel_id()?;
+// Some(call_channel == channel_id)
+// })
+// .unwrap_or(false);
+
+// const FACEPILE_LIMIT: usize = 3;
+
+// enum ChannelCall {}
+// enum ChannelNote {}
+// enum NotesTooltip {}
+// enum ChatTooltip {}
+// enum ChannelTooltip {}
+
+// let mut is_dragged_over = false;
+// if cx
+// .global::<DragAndDrop<Workspace>>()
+// .currently_dragged::<Channel>(cx.window())
+// .is_some()
+// && self.drag_target_channel == ChannelDragTarget::Channel(channel_id)
+// {
+// is_dragged_over = true;
+// }
+
+// let has_messages_notification = channel.unseen_message_id.is_some();
+
+// MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
+// let row_hovered = state.hovered();
+
+// let mut select_state = |interactive: &Interactive<ContainerStyle>| {
+// if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
+// interactive.clicked.as_ref().unwrap().clone()
+// } else if state.hovered() || other_selected {
+// interactive
+// .hovered
+// .as_ref()
+// .unwrap_or(&interactive.default)
+// .clone()
+// } else {
+// interactive.default.clone()
+// }
+// };
+
+// Flex::<Self>::row()
+// .with_child(
+// Svg::new(if is_public {
+// "icons/public.svg"
+// } else {
+// "icons/hash.svg"
+// })
+// .with_color(collab_theme.channel_hash.color)
+// .constrained()
+// .with_width(collab_theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child({
+// let style = collab_theme.channel_name.inactive_state();
+// Flex::row()
+// .with_child(
+// Label::new(channel.name.clone(), style.text.clone())
+// .contained()
+// .with_style(style.container)
+// .aligned()
+// .left()
+// .with_tooltip::<ChannelTooltip>(
+// ix,
+// "Join channel",
+// None,
+// theme.tooltip.clone(),
+// cx,
+// ),
+// )
+// .with_children({
+// let participants =
+// self.channel_store.read(cx).channel_participants(channel_id);
+
+// if !participants.is_empty() {
+// let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
+
+// let result = FacePile::new(collab_theme.face_overlap)
+// .with_children(
+// participants
+// .iter()
+// .filter_map(|user| {
+// Some(
+// Image::from_data(user.avatar.clone()?)
+// .with_style(collab_theme.channel_avatar),
+// )
+// })
+// .take(FACEPILE_LIMIT),
+// )
+// .with_children((extra_count > 0).then(|| {
+// Label::new(
+// format!("+{}", extra_count),
+// collab_theme.extra_participant_label.text.clone(),
+// )
+// .contained()
+// .with_style(collab_theme.extra_participant_label.container)
+// }));
+
+// Some(result)
+// } else {
+// None
+// }
+// })
+// .with_spacing(8.)
+// .align_children_center()
+// .flex(1., true)
+// })
+// .with_child(
+// MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
+// let container_style = collab_theme
+// .disclosure
+// .button
+// .style_for(mouse_state)
+// .container;
+
+// if channel.unseen_message_id.is_some() {
+// Svg::new("icons/conversations.svg")
+// .with_color(collab_theme.channel_note_active_color)
+// .constrained()
+// .with_width(collab_theme.channel_hash.width)
+// .contained()
+// .with_style(container_style)
+// .with_uniform_padding(4.)
+// .into_any()
+// } else if row_hovered {
+// Svg::new("icons/conversations.svg")
+// .with_color(collab_theme.channel_hash.color)
+// .constrained()
+// .with_width(collab_theme.channel_hash.width)
+// .contained()
+// .with_style(container_style)
+// .with_uniform_padding(4.)
+// .into_any()
+// } else {
+// Empty::new().into_any()
+// }
+// })
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+// })
+// .with_tooltip::<ChatTooltip>(
+// ix,
+// "Open channel chat",
+// None,
+// theme.tooltip.clone(),
+// cx,
+// )
+// .contained()
+// .with_margin_right(4.),
+// )
+// .with_child(
+// MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
+// let container_style = collab_theme
+// .disclosure
+// .button
+// .style_for(mouse_state)
+// .container;
+// if row_hovered || channel.unseen_note_version.is_some() {
+// Svg::new("icons/file.svg")
+// .with_color(if channel.unseen_note_version.is_some() {
+// collab_theme.channel_note_active_color
+// } else {
+// collab_theme.channel_hash.color
+// })
+// .constrained()
+// .with_width(collab_theme.channel_hash.width)
+// .contained()
+// .with_style(container_style)
+// .with_uniform_padding(4.)
+// .with_margin_right(collab_theme.channel_hash.container.margin.left)
+// .with_tooltip::<NotesTooltip>(
+// ix as usize,
+// "Open channel notes",
+// None,
+// theme.tooltip.clone(),
+// cx,
+// )
+// .into_any()
+// } else if has_messages_notification {
+// Empty::new()
+// .constrained()
+// .with_width(collab_theme.channel_hash.width)
+// .contained()
+// .with_uniform_padding(4.)
+// .with_margin_right(collab_theme.channel_hash.container.margin.left)
+// .into_any()
+// } else {
+// Empty::new().into_any()
+// }
+// })
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+// }),
+// )
+// .align_children_center()
+// .styleable_component()
+// .disclosable(
+// disclosed,
+// Box::new(ToggleCollapse {
+// location: channel.id.clone(),
+// }),
+// )
+// .with_id(ix)
+// .with_style(collab_theme.disclosure.clone())
+// .element()
+// .constrained()
+// .with_height(collab_theme.row_height)
+// .contained()
+// .with_style(select_state(
+// collab_theme
+// .channel_row
+// .in_state(is_selected || is_active || is_dragged_over),
+// ))
+// .with_padding_left(
+// collab_theme.channel_row.default_style().padding.left
+// + collab_theme.channel_indent * depth as f32,
+// )
+// })
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// if this.drag_target_channel == ChannelDragTarget::None {
+// if is_active {
+// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
+// } else {
+// this.join_channel(channel_id, cx)
+// }
+// }
+// })
+// .on_click(MouseButton::Right, {
+// let channel = channel.clone();
+// move |e, this, cx| {
+// this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx);
+// }
+// })
+// .on_up(MouseButton::Left, move |_, this, cx| {
+// if let Some((_, dragged_channel)) = cx
+// .global::<DragAndDrop<Workspace>>()
+// .currently_dragged::<Channel>(cx.window())
+// {
+// this.channel_store
+// .update(cx, |channel_store, cx| {
+// channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
+// })
+// .detach_and_log_err(cx)
+// }
+// })
+// .on_move({
+// let channel = channel.clone();
+// move |_, this, cx| {
+// if let Some((_, dragged_channel)) = cx
+// .global::<DragAndDrop<Workspace>>()
+// .currently_dragged::<Channel>(cx.window())
+// {
+// if channel.id != dragged_channel.id {
+// this.drag_target_channel = ChannelDragTarget::Channel(channel.id);
+// }
+// cx.notify()
+// }
+// }
+// })
+// .as_draggable::<_, Channel>(
+// channel.clone(),
+// move |_, channel, cx: &mut ViewContext<Workspace>| {
+// let theme = &theme::current(cx).collab_panel;
+
+// Flex::<Workspace>::row()
+// .with_child(
+// Svg::new("icons/hash.svg")
+// .with_color(theme.channel_hash.color)
+// .constrained()
+// .with_width(theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new(channel.name.clone(), theme.channel_name.text.clone())
+// .contained()
+// .with_style(theme.channel_name.container)
+// .aligned()
+// .left(),
+// )
+// .align_children_center()
+// .contained()
+// .with_background_color(
+// theme
+// .container
+// .background_color
+// .unwrap_or(gpui::color::Color::transparent_black()),
+// )
+// .contained()
+// .with_padding_left(
+// theme.channel_row.default_style().padding.left
+// + theme.channel_indent * depth as f32,
+// )
+// .into_any()
+// },
+// )
+// .with_cursor_style(CursorStyle::PointingHand)
+// .into_any()
+// }
+
+// fn render_channel_notes(
+// &self,
+// channel_id: ChannelId,
+// theme: &theme::CollabPanel,
+// is_selected: bool,
+// ix: usize,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum ChannelNotes {}
+// let host_avatar_width = theme
+// .contact_avatar
+// .width
+// .or(theme.contact_avatar.height)
+// .unwrap_or(0.);
+
+// MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
+// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+// let row = theme.project_row.in_state(is_selected).style_for(state);
+
+// Flex::<Self>::row()
+// .with_child(render_tree_branch(
+// tree_branch,
+// &row.name.text,
+// false,
+// vec2f(host_avatar_width, theme.row_height),
+// cx.font_cache(),
+// ))
+// .with_child(
+// Svg::new("icons/file.svg")
+// .with_color(theme.channel_hash.color)
+// .constrained()
+// .with_width(theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new("notes", theme.channel_name.text.clone())
+// .contained()
+// .with_style(theme.channel_name.container)
+// .aligned()
+// .left()
+// .flex(1., true),
+// )
+// .constrained()
+// .with_height(theme.row_height)
+// .contained()
+// .with_style(*theme.channel_row.style_for(is_selected, state))
+// .with_padding_left(theme.channel_row.default_style().padding.left)
+// })
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .into_any()
+// }
+
+// fn render_channel_chat(
+// &self,
+// channel_id: ChannelId,
+// theme: &theme::CollabPanel,
+// is_selected: bool,
+// ix: usize,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum ChannelChat {}
+// let host_avatar_width = theme
+// .contact_avatar
+// .width
+// .or(theme.contact_avatar.height)
+// .unwrap_or(0.);
+
+// MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
+// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+// let row = theme.project_row.in_state(is_selected).style_for(state);
+
+// Flex::<Self>::row()
+// .with_child(render_tree_branch(
+// tree_branch,
+// &row.name.text,
+// true,
+// vec2f(host_avatar_width, theme.row_height),
+// cx.font_cache(),
+// ))
+// .with_child(
+// Svg::new("icons/conversations.svg")
+// .with_color(theme.channel_hash.color)
+// .constrained()
+// .with_width(theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new("chat", theme.channel_name.text.clone())
+// .contained()
+// .with_style(theme.channel_name.container)
+// .aligned()
+// .left()
+// .flex(1., true),
+// )
+// .constrained()
+// .with_height(theme.row_height)
+// .contained()
+// .with_style(*theme.channel_row.style_for(is_selected, state))
+// .with_padding_left(theme.channel_row.default_style().padding.left)
+// })
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .into_any()
+// }
+
+// fn render_channel_invite(
+// channel: Arc<Channel>,
+// channel_store: ModelHandle<ChannelStore>,
+// theme: &theme::CollabPanel,
+// is_selected: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum Decline {}
+// enum Accept {}
+
+// let channel_id = channel.id;
+// let is_invite_pending = channel_store
+// .read(cx)
+// .has_pending_channel_invite_response(&channel);
+// let button_spacing = theme.contact_button_spacing;
+
+// Flex::row()
+// .with_child(
+// Svg::new("icons/hash.svg")
+// .with_color(theme.channel_hash.color)
+// .constrained()
+// .with_width(theme.channel_hash.width)
+// .aligned()
+// .left(),
+// )
+// .with_child(
+// Label::new(channel.name.clone(), theme.contact_username.text.clone())
+// .contained()
+// .with_style(theme.contact_username.container)
+// .aligned()
+// .left()
+// .flex(1., true),
+// )
+// .with_child(
+// MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
+// let button_style = if is_invite_pending {
+// &theme.disabled_button
+// } else {
+// theme.contact_button.style_for(mouse_state)
+// };
+// render_icon_button(button_style, "icons/x.svg").aligned()
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.respond_to_channel_invite(channel_id, false, cx);
+// })
+// .contained()
+// .with_margin_right(button_spacing),
+// )
+// .with_child(
+// MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
+// let button_style = if is_invite_pending {
+// &theme.disabled_button
+// } else {
+// theme.contact_button.style_for(mouse_state)
+// };
+// render_icon_button(button_style, "icons/check.svg")
+// .aligned()
+// .flex_float()
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.respond_to_channel_invite(channel_id, true, cx);
+// }),
+// )
+// .constrained()
+// .with_height(theme.row_height)
+// .contained()
+// .with_style(
+// *theme
+// .contact_row
+// .in_state(is_selected)
+// .style_for(&mut Default::default()),
+// )
+// .with_padding_left(
+// theme.contact_row.default_style().padding.left + theme.channel_indent,
+// )
+// .into_any()
+// }
+
+// fn render_contact_request(
+// user: Arc<User>,
+// user_store: ModelHandle<UserStore>,
+// theme: &theme::CollabPanel,
+// is_incoming: bool,
+// is_selected: bool,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum Decline {}
+// enum Accept {}
+// enum Cancel {}
+
+// let mut row = Flex::row()
+// .with_children(user.avatar.clone().map(|avatar| {
+// Image::from_data(avatar)
+// .with_style(theme.contact_avatar)
+// .aligned()
+// .left()
+// }))
+// .with_child(
+// Label::new(
+// user.github_login.clone(),
+// theme.contact_username.text.clone(),
+// )
+// .contained()
+// .with_style(theme.contact_username.container)
+// .aligned()
+// .left()
+// .flex(1., true),
+// );
+
+// let user_id = user.id;
+// let github_login = user.github_login.clone();
+// let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
+// let button_spacing = theme.contact_button_spacing;
+
+// if is_incoming {
+// row.add_child(
+// MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
+// let button_style = if is_contact_request_pending {
+// &theme.disabled_button
+// } else {
+// theme.contact_button.style_for(mouse_state)
+// };
+// render_icon_button(button_style, "icons/x.svg").aligned()
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.respond_to_contact_request(user_id, false, cx);
+// })
+// .contained()
+// .with_margin_right(button_spacing),
+// );
+
+// row.add_child(
+// MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
+// let button_style = if is_contact_request_pending {
+// &theme.disabled_button
+// } else {
+// theme.contact_button.style_for(mouse_state)
+// };
+// render_icon_button(button_style, "icons/check.svg")
+// .aligned()
+// .flex_float()
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.respond_to_contact_request(user_id, true, cx);
+// }),
+// );
+// } else {
+// row.add_child(
+// MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
+// let button_style = if is_contact_request_pending {
+// &theme.disabled_button
+// } else {
+// theme.contact_button.style_for(mouse_state)
+// };
+// render_icon_button(button_style, "icons/x.svg")
+// .aligned()
+// .flex_float()
+// })
+// .with_padding(Padding::uniform(2.))
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.remove_contact(user_id, &github_login, cx);
+// })
+// .flex_float(),
+// );
+// }
+
+// row.constrained()
+// .with_height(theme.row_height)
+// .contained()
+// .with_style(
+// *theme
+// .contact_row
+// .in_state(is_selected)
+// .style_for(&mut Default::default()),
+// )
+// .into_any()
+// }
+
+// fn has_subchannels(&self, ix: usize) -> bool {
+// self.entries.get(ix).map_or(false, |entry| {
+// if let ListEntry::Channel { has_children, .. } = entry {
+// *has_children
+// } else {
+// false
+// }
+// })
+// }
+
+// fn deploy_channel_context_menu(
+// &mut self,
+// position: Option<Vector2F>,
+// channel: &Channel,
+// ix: usize,
+// cx: &mut ViewContext<Self>,
+// ) {
+// self.context_menu_on_selected = position.is_none();
+
+// let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
+// self.channel_store
+// .read(cx)
+// .channel_for_id(clipboard.channel_id)
+// .map(|channel| channel.name.clone())
+// });
+
+// self.context_menu.update(cx, |context_menu, cx| {
+// context_menu.set_position_mode(if self.context_menu_on_selected {
+// OverlayPositionMode::Local
+// } else {
+// OverlayPositionMode::Window
+// });
+
+// let mut items = Vec::new();
+
+// let select_action_name = if self.selection == Some(ix) {
+// "Unselect"
+// } else {
+// "Select"
+// };
+
+// items.push(ContextMenuItem::action(
+// select_action_name,
+// ToggleSelectedIx { ix },
+// ));
+
+// if self.has_subchannels(ix) {
+// let expand_action_name = if self.is_channel_collapsed(channel.id) {
+// "Expand Subchannels"
+// } else {
+// "Collapse Subchannels"
+// };
+// items.push(ContextMenuItem::action(
+// expand_action_name,
+// ToggleCollapse {
+// location: channel.id,
+// },
+// ));
+// }
+
+// items.push(ContextMenuItem::action(
+// "Open Notes",
+// OpenChannelNotes {
+// channel_id: channel.id,
+// },
+// ));
+
+// items.push(ContextMenuItem::action(
+// "Open Chat",
+// JoinChannelChat {
+// channel_id: channel.id,
+// },
+// ));
+
+// items.push(ContextMenuItem::action(
+// "Copy Channel Link",
+// CopyChannelLink {
+// channel_id: channel.id,
+// },
+// ));
+
+// if self.channel_store.read(cx).is_channel_admin(channel.id) {
+// items.extend([
+// ContextMenuItem::Separator,
+// ContextMenuItem::action(
+// "New Subchannel",
+// NewChannel {
+// location: channel.id,
+// },
+// ),
+// ContextMenuItem::action(
+// "Rename",
+// RenameChannel {
+// channel_id: channel.id,
+// },
+// ),
+// ContextMenuItem::action(
+// "Move this channel",
+// StartMoveChannelFor {
+// channel_id: channel.id,
+// },
+// ),
+// ]);
+
+// if let Some(channel_name) = clipboard_channel_name {
+// items.push(ContextMenuItem::Separator);
+// items.push(ContextMenuItem::action(
+// format!("Move '#{}' here", channel_name),
+// MoveChannel { to: channel.id },
+// ));
+// }
+
+// items.extend([
+// ContextMenuItem::Separator,
+// ContextMenuItem::action(
+// "Invite Members",
+// InviteMembers {
+// channel_id: channel.id,
+// },
+// ),
+// ContextMenuItem::action(
+// "Manage Members",
+// ManageMembers {
+// channel_id: channel.id,
+// },
+// ),
+// ContextMenuItem::Separator,
+// ContextMenuItem::action(
+// "Delete",
+// RemoveChannel {
+// channel_id: channel.id,
+// },
+// ),
+// ]);
+// }
+
+// context_menu.show(
+// position.unwrap_or_default(),
+// if self.context_menu_on_selected {
+// gpui::elements::AnchorCorner::TopRight
+// } else {
+// gpui::elements::AnchorCorner::BottomLeft
+// },
+// items,
+// cx,
+// );
+// });
+
+// cx.notify();
+// }
+
+// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+// if self.take_editing_state(cx) {
+// cx.focus(&self.filter_editor);
+// } else {
+// self.filter_editor.update(cx, |editor, cx| {
+// if editor.buffer().read(cx).len(cx) > 0 {
+// editor.set_text("", cx);
+// }
+// });
+// }
+
+// self.update_entries(false, cx);
+// }
+
+// fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+// let ix = self.selection.map_or(0, |ix| ix + 1);
+// if ix < self.entries.len() {
+// self.selection = Some(ix);
+// }
+
+// self.list_state.reset(self.entries.len());
+// if let Some(ix) = self.selection {
+// self.list_state.scroll_to(ListOffset {
+// item_ix: ix,
+// offset_in_item: 0.,
+// });
+// }
+// cx.notify();
+// }
+
+// fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+// let ix = self.selection.take().unwrap_or(0);
+// if ix > 0 {
+// self.selection = Some(ix - 1);
+// }
+
+// self.list_state.reset(self.entries.len());
+// if let Some(ix) = self.selection {
+// self.list_state.scroll_to(ListOffset {
+// item_ix: ix,
+// offset_in_item: 0.,
+// });
+// }
+// cx.notify();
+// }
+
+// fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+// if self.confirm_channel_edit(cx) {
+// return;
+// }
+
+// if let Some(selection) = self.selection {
+// if let Some(entry) = self.entries.get(selection) {
+// match entry {
+// ListEntry::Header(section) => match section {
+// Section::ActiveCall => Self::leave_call(cx),
+// Section::Channels => self.new_root_channel(cx),
+// Section::Contacts => self.toggle_contact_finder(cx),
+// Section::ContactRequests
+// | Section::Online
+// | Section::Offline
+// | Section::ChannelInvites => {
+// self.toggle_section_expanded(*section, cx);
+// }
+// },
+// ListEntry::Contact { contact, calling } => {
+// if contact.online && !contact.busy && !calling {
+// self.call(contact.user.id, Some(self.project.clone()), cx);
+// }
+// }
+// ListEntry::ParticipantProject {
+// project_id,
+// host_user_id,
+// ..
+// } => {
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// let app_state = workspace.read(cx).app_state().clone();
+// workspace::join_remote_project(
+// *project_id,
+// *host_user_id,
+// app_state,
+// cx,
+// )
+// .detach_and_log_err(cx);
+// }
+// }
+// ListEntry::ParticipantScreen { peer_id, .. } => {
+// let Some(peer_id) = peer_id else {
+// return;
+// };
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// workspace.update(cx, |workspace, cx| {
+// workspace.open_shared_screen(*peer_id, cx)
+// });
+// }
+// }
+// ListEntry::Channel { channel, .. } => {
+// let is_active = maybe!({
+// let call_channel = ActiveCall::global(cx)
+// .read(cx)
+// .room()?
+// .read(cx)
+// .channel_id()?;
+
+// Some(call_channel == channel.id)
+// })
+// .unwrap_or(false);
+// if is_active {
+// self.open_channel_notes(
+// &OpenChannelNotes {
+// channel_id: channel.id,
+// },
+// cx,
+// )
+// } else {
+// self.join_channel(channel.id, cx)
+// }
+// }
+// ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
+// _ => {}
+// }
+// }
+// }
+// }
+
+// fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
+// if self.channel_editing_state.is_some() {
+// self.channel_name_editor.update(cx, |editor, cx| {
+// editor.insert(" ", cx);
+// });
+// }
+// }
+
+// fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
+// if let Some(editing_state) = &mut self.channel_editing_state {
+// match editing_state {
+// ChannelEditingState::Create {
+// location,
+// pending_name,
+// ..
+// } => {
+// if pending_name.is_some() {
+// return false;
+// }
+// let channel_name = self.channel_name_editor.read(cx).text(cx);
+
+// *pending_name = Some(channel_name.clone());
+
+// self.channel_store
+// .update(cx, |channel_store, cx| {
+// channel_store.create_channel(&channel_name, *location, cx)
+// })
+// .detach();
+// cx.notify();
+// }
+// ChannelEditingState::Rename {
+// location,
+// pending_name,
+// } => {
+// if pending_name.is_some() {
+// return false;
+// }
+// let channel_name = self.channel_name_editor.read(cx).text(cx);
+// *pending_name = Some(channel_name.clone());
+
+// self.channel_store
+// .update(cx, |channel_store, cx| {
+// channel_store.rename(*location, &channel_name, cx)
+// })
+// .detach();
+// cx.notify();
+// }
+// }
+// cx.focus_self();
+// true
+// } else {
+// false
+// }
+// }
+
+// fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
+// if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
+// self.collapsed_sections.remove(ix);
+// } else {
+// self.collapsed_sections.push(section);
+// }
+// self.update_entries(false, cx);
+// }
+
+// fn collapse_selected_channel(
+// &mut self,
+// _: &CollapseSelectedChannel,
+// cx: &mut ViewContext<Self>,
+// ) {
+// let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+// return;
+// };
+
+// if self.is_channel_collapsed(channel_id) {
+// return;
+// }
+
+// self.toggle_channel_collapsed(channel_id, cx);
+// }
+
+// fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
+// let Some(id) = self.selected_channel().map(|channel| channel.id) else {
+// return;
+// };
+
+// if !self.is_channel_collapsed(id) {
+// return;
+// }
+
+// self.toggle_channel_collapsed(id, cx)
+// }
+
+// fn toggle_channel_collapsed_action(
+// &mut self,
+// action: &ToggleCollapse,
+// cx: &mut ViewContext<Self>,
+// ) {
+// self.toggle_channel_collapsed(action.location, cx);
+// }
+
+// fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+// match self.collapsed_channels.binary_search(&channel_id) {
+// Ok(ix) => {
+// self.collapsed_channels.remove(ix);
+// }
+// Err(ix) => {
+// self.collapsed_channels.insert(ix, channel_id);
+// }
+// };
+// self.serialize(cx);
+// self.update_entries(true, cx);
+// cx.notify();
+// cx.focus_self();
+// }
+
+// fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
+// self.collapsed_channels.binary_search(&channel_id).is_ok()
+// }
+
+// fn leave_call(cx: &mut ViewContext<Self>) {
+// ActiveCall::global(cx)
+// .update(cx, |call, cx| call.hang_up(cx))
+// .detach_and_log_err(cx);
+// }
+
+// fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// workspace.update(cx, |workspace, cx| {
+// workspace.toggle_modal(cx, |_, cx| {
+// cx.add_view(|cx| {
+// let mut finder = ContactFinder::new(self.user_store.clone(), cx);
+// finder.set_query(self.filter_editor.read(cx).text(cx), cx);
+// finder
+// })
+// });
+// });
+// }
+// }
+
+// fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
+// self.channel_editing_state = Some(ChannelEditingState::Create {
+// location: None,
+// pending_name: None,
+// });
+// self.update_entries(false, cx);
+// self.select_channel_editor();
+// cx.focus(self.channel_name_editor.as_any());
+// cx.notify();
+// }
+
+// fn select_channel_editor(&mut self) {
+// self.selection = self.entries.iter().position(|entry| match entry {
+// ListEntry::ChannelEditor { .. } => true,
+// _ => false,
+// });
+// }
+
+// fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
+// self.collapsed_channels
+// .retain(|channel| *channel != action.location);
+// self.channel_editing_state = Some(ChannelEditingState::Create {
+// location: Some(action.location.to_owned()),
+// pending_name: None,
+// });
+// self.update_entries(false, cx);
+// self.select_channel_editor();
+// cx.focus(self.channel_name_editor.as_any());
+// cx.notify();
+// }
+
+// fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
+// self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
+// }
+
+// fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
+// self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
+// }
+
+// fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+// if let Some(channel) = self.selected_channel() {
+// self.remove_channel(channel.id, cx)
+// }
+// }
+
+// fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+// if let Some(channel) = self.selected_channel() {
+// self.rename_channel(
+// &RenameChannel {
+// channel_id: channel.id,
+// },
+// cx,
+// );
+// }
+// }
+
+// fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
+// let channel_store = self.channel_store.read(cx);
+// if !channel_store.is_channel_admin(action.channel_id) {
+// return;
+// }
+// if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
+// self.channel_editing_state = Some(ChannelEditingState::Rename {
+// location: action.channel_id.to_owned(),
+// pending_name: None,
+// });
+// self.channel_name_editor.update(cx, |editor, cx| {
+// editor.set_text(channel.name.clone(), cx);
+// editor.select_all(&Default::default(), cx);
+// });
+// cx.focus(self.channel_name_editor.as_any());
+// self.update_entries(false, cx);
+// self.select_channel_editor();
+// }
+// }
+
+// fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// ChannelView::open(action.channel_id, workspace, cx).detach();
+// }
+// }
+
+// fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
+// let Some(channel) = self.selected_channel() else {
+// return;
+// };
+
+// self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx);
+// }
+
+// fn selected_channel(&self) -> Option<&Arc<Channel>> {
+// self.selection
+// .and_then(|ix| self.entries.get(ix))
+// .and_then(|entry| match entry {
+// ListEntry::Channel { channel, .. } => Some(channel),
+// _ => None,
+// })
+// }
+
+// fn show_channel_modal(
+// &mut self,
+// channel_id: ChannelId,
+// mode: channel_modal::Mode,
+// cx: &mut ViewContext<Self>,
+// ) {
+// let workspace = self.workspace.clone();
+// let user_store = self.user_store.clone();
+// let channel_store = self.channel_store.clone();
+// let members = self.channel_store.update(cx, |channel_store, cx| {
+// channel_store.get_channel_member_details(channel_id, cx)
+// });
+
+// cx.spawn(|_, mut cx| async move {
+// let members = members.await?;
+// workspace.update(&mut cx, |workspace, cx| {
+// workspace.toggle_modal(cx, |_, cx| {
+// cx.add_view(|cx| {
+// ChannelModal::new(
+// user_store.clone(),
+// channel_store.clone(),
+// channel_id,
+// mode,
+// members,
+// cx,
+// )
+// })
+// });
+// })
+// })
+// .detach();
+// }
+
+// fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
+// self.remove_channel(action.channel_id, cx)
+// }
+
+// fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+// let channel_store = self.channel_store.clone();
+// if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
+// let prompt_message = format!(
+// "Are you sure you want to remove the channel \"{}\"?",
+// channel.name
+// );
+// let mut answer =
+// cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+// let window = cx.window();
+// cx.spawn(|this, mut cx| async move {
+// if answer.next().await == Some(0) {
+// if let Err(e) = channel_store
+// .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
+// .await
+// {
+// window.prompt(
+// PromptLevel::Info,
+// &format!("Failed to remove channel: {}", e),
+// &["Ok"],
+// &mut cx,
+// );
+// }
+// this.update(&mut cx, |_, cx| cx.focus_self()).ok();
+// }
+// })
+// .detach();
+// }
+// }
+
+// // Should move to the filter editor if clicking on it
+// // Should move selection to the channel editor if activating it
+
+// fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
+// let user_store = self.user_store.clone();
+// let prompt_message = format!(
+// "Are you sure you want to remove \"{}\" from your contacts?",
+// github_login
+// );
+// let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+// let window = cx.window();
+// cx.spawn(|_, mut cx| async move {
+// if answer.next().await == Some(0) {
+// if let Err(e) = user_store
+// .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
+// .await
+// {
+// window.prompt(
+// PromptLevel::Info,
+// &format!("Failed to remove contact: {}", e),
+// &["Ok"],
+// &mut cx,
+// );
+// }
+// }
+// })
+// .detach();
+// }
+
+// fn respond_to_contact_request(
+// &mut self,
+// user_id: u64,
+// accept: bool,
+// cx: &mut ViewContext<Self>,
+// ) {
+// self.user_store
+// .update(cx, |store, cx| {
+// store.respond_to_contact_request(user_id, accept, cx)
+// })
+// .detach();
+// }
+
+// fn respond_to_channel_invite(
+// &mut self,
+// channel_id: u64,
+// accept: bool,
+// cx: &mut ViewContext<Self>,
+// ) {
+// self.channel_store
+// .update(cx, |store, cx| {
+// store.respond_to_channel_invite(channel_id, accept, cx)
+// })
+// .detach();
+// }
+
+// fn call(
+// &mut self,
+// recipient_user_id: u64,
+// initial_project: Option<ModelHandle<Project>>,
+// cx: &mut ViewContext<Self>,
+// ) {
+// ActiveCall::global(cx)
+// .update(cx, |call, cx| {
+// call.invite(recipient_user_id, initial_project, cx)
+// })
+// .detach_and_log_err(cx);
+// }
+
+// fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
+// let Some(workspace) = self.workspace.upgrade(cx) else {
+// return;
+// };
+// let Some(handle) = cx.window().downcast::<Workspace>() else {
+// return;
+// };
+// workspace::join_channel(
+// channel_id,
+// workspace.read(cx).app_state().clone(),
+// Some(handle),
+// cx,
+// )
+// .detach_and_log_err(cx)
+// }
+
+// fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
+// let channel_id = action.channel_id;
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// cx.app_context().defer(move |cx| {
+// workspace.update(cx, |workspace, cx| {
+// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+// panel.update(cx, |panel, cx| {
+// panel
+// .select_channel(channel_id, None, cx)
+// .detach_and_log_err(cx);
+// });
+// }
+// });
+// });
+// }
+// }
+
+// fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
+// let channel_store = self.channel_store.read(cx);
+// let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
+// return;
+// };
+// let item = ClipboardItem::new(channel.link());
+// cx.write_to_clipboard(item)
+// }
+// }
+
+// fn render_tree_branch(
+// branch_style: theme::TreeBranch,
+// row_style: &TextStyle,
+// is_last: bool,
+// size: Vector2F,
+// font_cache: &FontCache,
+// ) -> gpui::elements::ConstrainedBox<CollabPanel> {
+// let line_height = row_style.line_height(font_cache);
+// let cap_height = row_style.cap_height(font_cache);
+// let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
+
+// Canvas::new(move |bounds, _, _, cx| {
+// cx.paint_layer(None, |cx| {
+// let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
+// let end_x = bounds.max_x();
+// let start_y = bounds.min_y();
+// let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+// cx.scene().push_quad(gpui::Quad {
+// bounds: RectF::from_points(
+// vec2f(start_x, start_y),
+// vec2f(
+// start_x + branch_style.width,
+// if is_last { end_y } else { bounds.max_y() },
+// ),
+// ),
+// background: Some(branch_style.color),
+// border: gpui::Border::default(),
+// corner_radii: (0.).into(),
+// });
+// cx.scene().push_quad(gpui::Quad {
+// bounds: RectF::from_points(
+// vec2f(start_x, end_y),
+// vec2f(end_x, end_y + branch_style.width),
+// ),
+// background: Some(branch_style.color),
+// border: gpui::Border::default(),
+// corner_radii: (0.).into(),
+// });
+// })
+// })
+// .constrained()
+// .with_width(size.x())
+// }
+
+// impl View for CollabPanel {
+// fn ui_name() -> &'static str {
+// "CollabPanel"
+// }
+
+// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+// if !self.has_focus {
+// self.has_focus = true;
+// if !self.context_menu.is_focused(cx) {
+// if let Some(editing_state) = &self.channel_editing_state {
+// if editing_state.pending_name().is_none() {
+// cx.focus(&self.channel_name_editor);
+// } else {
+// cx.focus(&self.filter_editor);
+// }
+// } else {
+// cx.focus(&self.filter_editor);
+// }
+// }
+// cx.emit(Event::Focus);
+// }
+// }
+
+// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+// self.has_focus = false;
+// }
+
+// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+// let theme = &theme::current(cx).collab_panel;
+
+// if self.user_store.read(cx).current_user().is_none() {
+// enum LogInButton {}
+
+// return Flex::column()
+// .with_child(
+// MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
+// let button = theme.log_in_button.style_for(state);
+// Label::new("Sign in to collaborate", button.text.clone())
+// .aligned()
+// .left()
+// .contained()
+// .with_style(button.container)
+// })
+// .on_click(MouseButton::Left, |_, this, cx| {
+// let client = this.client.clone();
+// cx.spawn(|_, cx| async move {
+// client.authenticate_and_connect(true, &cx).await.log_err();
+// })
+// .detach();
+// })
+// .with_cursor_style(CursorStyle::PointingHand),
+// )
+// .contained()
+// .with_style(theme.container)
+// .into_any();
+// }
+
+// enum PanelFocus {}
+// MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
+// Stack::new()
+// .with_child(
+// Flex::column()
+// .with_child(
+// Flex::row().with_child(
+// ChildView::new(&self.filter_editor, cx)
+// .contained()
+// .with_style(theme.user_query_editor.container)
+// .flex(1.0, true),
+// ),
+// )
+// .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
+// .contained()
+// .with_style(theme.container)
+// .into_any(),
+// )
+// .with_children(
+// (!self.context_menu_on_selected)
+// .then(|| ChildView::new(&self.context_menu, cx)),
+// )
+// .into_any()
+// })
+// .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
+// .into_any_named("collab panel")
+// }
+
+// fn update_keymap_context(
+// &self,
+// keymap: &mut gpui::keymap_matcher::KeymapContext,
+// _: &AppContext,
+// ) {
+// Self::reset_to_default_keymap_context(keymap);
+// if self.channel_editing_state.is_some() {
+// keymap.add_identifier("editing");
+// } else {
+// keymap.add_identifier("not_editing");
+// }
+// }
+// }
+
+// impl Panel for CollabPanel {
+// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+// settings::get::<CollaborationPanelSettings>(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::<CollaborationPanelSettings>(
+// self.fs.clone(),
+// cx,
+// move |settings| settings.dock = Some(position),
+// );
+// }
+
+// fn size(&self, cx: &gpui::WindowContext) -> f32 {
+// self.width
+// .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
+// }
+
+// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+// self.width = size;
+// self.serialize(cx);
+// cx.notify();
+// }
+
+// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+// settings::get::<CollaborationPanelSettings>(cx)
+// .button
+// .then(|| "icons/user_group_16.svg")
+// }
+
+// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+// (
+// "Collaboration Panel".to_string(),
+// Some(Box::new(ToggleFocus)),
+// )
+// }
+
+// fn should_change_position_on_event(event: &Self::Event) -> bool {
+// matches!(event, Event::DockPositionChanged)
+// }
+
+// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+// self.has_focus
+// }
+
+// fn is_focus_event(event: &Self::Event) -> bool {
+// matches!(event, Event::Focus)
+// }
+// }
+
+// impl PartialEq for ListEntry {
+// fn eq(&self, other: &Self) -> bool {
+// match self {
+// ListEntry::Header(section_1) => {
+// if let ListEntry::Header(section_2) = other {
+// return section_1 == section_2;
+// }
+// }
+// ListEntry::CallParticipant { user: user_1, .. } => {
+// if let ListEntry::CallParticipant { user: user_2, .. } = other {
+// return user_1.id == user_2.id;
+// }
+// }
+// ListEntry::ParticipantProject {
+// project_id: project_id_1,
+// ..
+// } => {
+// if let ListEntry::ParticipantProject {
+// project_id: project_id_2,
+// ..
+// } = other
+// {
+// return project_id_1 == project_id_2;
+// }
+// }
+// ListEntry::ParticipantScreen {
+// peer_id: peer_id_1, ..
+// } => {
+// if let ListEntry::ParticipantScreen {
+// peer_id: peer_id_2, ..
+// } = other
+// {
+// return peer_id_1 == peer_id_2;
+// }
+// }
+// ListEntry::Channel {
+// channel: channel_1, ..
+// } => {
+// if let ListEntry::Channel {
+// channel: channel_2, ..
+// } = other
+// {
+// return channel_1.id == channel_2.id;
+// }
+// }
+// ListEntry::ChannelNotes { channel_id } => {
+// if let ListEntry::ChannelNotes {
+// channel_id: other_id,
+// } = other
+// {
+// return channel_id == other_id;
+// }
+// }
+// ListEntry::ChannelChat { channel_id } => {
+// if let ListEntry::ChannelChat {
+// channel_id: other_id,
+// } = other
+// {
+// return channel_id == other_id;
+// }
+// }
+// ListEntry::ChannelInvite(channel_1) => {
+// if let ListEntry::ChannelInvite(channel_2) = other {
+// return channel_1.id == channel_2.id;
+// }
+// }
+// ListEntry::IncomingRequest(user_1) => {
+// if let ListEntry::IncomingRequest(user_2) = other {
+// return user_1.id == user_2.id;
+// }
+// }
+// ListEntry::OutgoingRequest(user_1) => {
+// if let ListEntry::OutgoingRequest(user_2) = other {
+// return user_1.id == user_2.id;
+// }
+// }
+// ListEntry::Contact {
+// contact: contact_1, ..
+// } => {
+// if let ListEntry::Contact {
+// contact: contact_2, ..
+// } = other
+// {
+// return contact_1.user.id == contact_2.user.id;
+// }
+// }
+// ListEntry::ChannelEditor { depth } => {
+// if let ListEntry::ChannelEditor { depth: other_depth } = other {
+// return depth == other_depth;
+// }
+// }
+// ListEntry::ContactPlaceholder => {
+// if let ListEntry::ContactPlaceholder = other {
+// return true;
+// }
+// }
+// }
+// false
+// }
+// }
+
+// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
+// 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)
+// }
@@ -1,162 +1,255 @@
-use crate::{
- face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
- ToggleDeafen, ToggleMute, ToggleScreenSharing,
-};
-use auto_update::AutoUpdateStatus;
-use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
-use clock::ReplicaId;
-use context_menu::{ContextMenu, ContextMenuItem};
+// use crate::{
+// face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
+// ToggleDeafen, ToggleMute, ToggleScreenSharing,
+// };
+// use auto_update::AutoUpdateStatus;
+// use call::{ActiveCall, ParticipantLocation, Room};
+// use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
+// use clock::ReplicaId;
+// use context_menu::{ContextMenu, ContextMenuItem};
+// use gpui::{
+// actions,
+// color::Color,
+// elements::*,
+// geometry::{rect::RectF, vector::vec2f, PathBuilder},
+// json::{self, ToJson},
+// platform::{CursorStyle, MouseButton},
+// AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
+// WeakViewHandle,
+// };
+// use picker::PickerEvent;
+// use project::{Project, RepositoryEntry};
+// use recent_projects::{build_recent_projects, RecentProjects};
+// use std::{ops::Range, sync::Arc};
+// use theme::{AvatarStyle, Theme};
+// use util::ResultExt;
+// use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
+// use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
+
+use std::sync::Arc;
+
+use call::ActiveCall;
+use client::{Client, UserStore};
use gpui::{
- actions,
- color::Color,
- elements::*,
- geometry::{rect::RectF, vector::vec2f, PathBuilder},
- json::{self, ToJson},
- platform::{CursorStyle, MouseButton},
- AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
- WeakViewHandle,
+ div, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, Render,
+ Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, VisualContext,
+ WeakView, WindowBounds,
};
-use picker::PickerEvent;
-use project::{Project, RepositoryEntry};
-use recent_projects::{build_recent_projects, RecentProjects};
-use std::{ops::Range, sync::Arc};
-use theme::{AvatarStyle, Theme};
-use util::ResultExt;
-use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
-
-const MAX_PROJECT_NAME_LENGTH: usize = 40;
-const MAX_BRANCH_NAME_LENGTH: usize = 40;
-
-actions!(
- collab,
- [
- ToggleUserMenu,
- ToggleProjectMenu,
- SwitchBranch,
- ShareProject,
- UnshareProject,
- ]
-);
+use project::Project;
+use theme::ActiveTheme;
+use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip};
+use workspace::Workspace;
+
+// const MAX_PROJECT_NAME_LENGTH: usize = 40;
+// const MAX_BRANCH_NAME_LENGTH: usize = 40;
+
+// actions!(
+// collab,
+// [
+// ToggleUserMenu,
+// ToggleProjectMenu,
+// SwitchBranch,
+// ShareProject,
+// UnshareProject,
+// ]
+// );
pub fn init(cx: &mut AppContext) {
- cx.add_action(CollabTitlebarItem::share_project);
- cx.add_action(CollabTitlebarItem::unshare_project);
- cx.add_action(CollabTitlebarItem::toggle_user_menu);
- cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
- cx.add_action(CollabTitlebarItem::toggle_project_menu);
+ cx.observe_new_views(|workspace: &mut Workspace, cx| {
+ let titlebar_item = cx.build_view(|cx| CollabTitlebarItem::new(workspace, cx));
+ workspace.set_titlebar_item(titlebar_item.into(), cx)
+ })
+ .detach();
+ // cx.add_action(CollabTitlebarItem::share_project);
+ // cx.add_action(CollabTitlebarItem::unshare_project);
+ // cx.add_action(CollabTitlebarItem::toggle_user_menu);
+ // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+ // cx.add_action(CollabTitlebarItem::toggle_project_menu);
}
pub struct CollabTitlebarItem {
- project: ModelHandle<Project>,
- user_store: ModelHandle<UserStore>,
+ project: Model<Project>,
+ #[allow(unused)] // todo!()
+ user_store: Model<UserStore>,
+ #[allow(unused)] // todo!()
client: Arc<Client>,
- workspace: WeakViewHandle<Workspace>,
- branch_popover: Option<ViewHandle<BranchList>>,
- project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
- user_menu: ViewHandle<ContextMenu>,
+ #[allow(unused)] // todo!()
+ workspace: WeakView<Workspace>,
+ //branch_popover: Option<ViewHandle<BranchList>>,
+ //project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
+ //user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
-impl Entity for CollabTitlebarItem {
- type Event = ();
-}
-
-impl View for CollabTitlebarItem {
- fn ui_name() -> &'static str {
- "CollabTitlebarItem"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
- workspace
- } else {
- return Empty::new().into_any();
- };
-
- let theme = theme::current(cx).clone();
- let mut left_container = Flex::row();
- let mut right_container = Flex::row().align_children_center();
-
- left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
-
- let user = self.user_store.read(cx).current_user();
- let peer_id = self.client.peer_id();
- if let Some(((user, peer_id), room)) = user
- .as_ref()
- .zip(peer_id)
- .zip(ActiveCall::global(cx).read(cx).room().cloned())
- {
- if room.read(cx).can_publish() {
- right_container
- .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
- }
- right_container.add_child(self.render_leave_call(&theme, cx));
- let muted = room.read(cx).is_muted(cx);
- let speaking = room.read(cx).is_speaking();
- left_container.add_child(
- self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
- );
- left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
- if room.read(cx).can_publish() {
- right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
- }
- right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
- if room.read(cx).can_publish() {
- right_container
- .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
- }
- }
-
- let status = workspace.read(cx).client().status();
- let status = &*status.borrow();
- if matches!(status, client::Status::Connected { .. }) {
- let avatar = user.as_ref().and_then(|user| user.avatar.clone());
- right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
- } else {
- right_container.add_children(self.render_connection_status(status, cx));
- right_container.add_child(self.render_sign_in_button(&theme, cx));
- right_container.add_child(self.render_user_menu_button(&theme, None, cx));
- }
+impl Render for CollabTitlebarItem {
+ type Element = Stateful<Self, Div<Self>>;
- Stack::new()
- .with_child(left_container)
- .with_child(
- Flex::row()
- .with_child(
- right_container.contained().with_background_color(
- theme
- .titlebar
- .container
- .background_color
- .unwrap_or_else(|| Color::transparent_black()),
- ),
- )
- .aligned()
- .right(),
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ h_stack()
+ .id("titlebar")
+ .justify_between()
+ .when(
+ !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
+ |s| s.pl_20(),
)
- .into_any()
+ .w_full()
+ .h(rems(1.75))
+ .bg(cx.theme().colors().title_bar_background)
+ .on_click(|_, event, cx| {
+ if event.up.click_count == 2 {
+ cx.zoom_window();
+ }
+ })
+ .child(
+ h_stack()
+ // TODO - Add player menu
+ .child(
+ div()
+ .id("project_owner_indicator")
+ .child(
+ Button::new("player")
+ .variant(ButtonVariant::Ghost)
+ .color(Some(TextColor::Player(0))),
+ )
+ .tooltip(move |_, cx| {
+ cx.build_view(|_| TextTooltip::new("Toggle following"))
+ }),
+ )
+ // TODO - Add project menu
+ .child(
+ div()
+ .id("titlebar_project_menu_button")
+ .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+ .tooltip(move |_, cx| {
+ cx.build_view(|_| TextTooltip::new("Recent Projects"))
+ }),
+ )
+ // TODO - Add git menu
+ .child(
+ div()
+ .id("titlebar_git_menu_button")
+ .child(
+ Button::new("branch_name")
+ .variant(ButtonVariant::Ghost)
+ .color(Some(TextColor::Muted)),
+ )
+ .tooltip(move |_, cx| {
+ // todo!() Replace with real action.
+ #[gpui::action]
+ struct NoAction {}
+
+ cx.build_view(|_| {
+ TextTooltip::new("Recent Branches")
+ .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+ "cmd-b",
+ NoAction {},
+ None,
+ )))
+ .meta("Only local branches shown")
+ })
+ }),
+ ),
+ ) // self.titlebar_item
+ .child(h_stack().child(Label::new("Right side titlebar item")))
}
}
+// impl Entity for CollabTitlebarItem {
+// type Event = ();
+// }
+
+// impl View for CollabTitlebarItem {
+// fn ui_name() -> &'static str {
+// "CollabTitlebarItem"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
+// workspace
+// } else {
+// return Empty::new().into_any();
+// };
+
+// let theme = theme::current(cx).clone();
+// let mut left_container = Flex::row();
+// let mut right_container = Flex::row().align_children_center();
+
+// left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
+
+// let user = self.user_store.read(cx).current_user();
+// let peer_id = self.client.peer_id();
+// if let Some(((user, peer_id), room)) = user
+// .as_ref()
+// .zip(peer_id)
+// .zip(ActiveCall::global(cx).read(cx).room().cloned())
+// {
+// if room.read(cx).can_publish() {
+// right_container
+// .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+// }
+// right_container.add_child(self.render_leave_call(&theme, cx));
+// let muted = room.read(cx).is_muted(cx);
+// let speaking = room.read(cx).is_speaking();
+// left_container.add_child(
+// self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
+// );
+// left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
+// if room.read(cx).can_publish() {
+// right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
+// }
+// right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
+// if room.read(cx).can_publish() {
+// right_container
+// .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
+// }
+// }
+
+// let status = workspace.read(cx).client().status();
+// let status = &*status.borrow();
+// if matches!(status, client::Status::Connected { .. }) {
+// let avatar = user.as_ref().and_then(|user| user.avatar.clone());
+// right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
+// } else {
+// right_container.add_children(self.render_connection_status(status, cx));
+// right_container.add_child(self.render_sign_in_button(&theme, cx));
+// right_container.add_child(self.render_user_menu_button(&theme, None, cx));
+// }
+
+// Stack::new()
+// .with_child(left_container)
+// .with_child(
+// Flex::row()
+// .with_child(
+// right_container.contained().with_background_color(
+// theme
+// .titlebar
+// .container
+// .background_color
+// .unwrap_or_else(|| Color::transparent_black()),
+// ),
+// )
+// .aligned()
+// .right(),
+// )
+// .into_any()
+// }
+// }
+
impl CollabTitlebarItem {
- pub fn new(
- workspace: &Workspace,
- workspace_handle: &ViewHandle<Workspace>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
+ pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project().clone();
let user_store = workspace.app_state().user_store.clone();
let client = workspace.app_state().client.clone();
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
- subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
+ subscriptions.push(
+ cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
+ cx.notify()
+ }),
+ );
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
- subscriptions.push(cx.observe_window_activation(|this, active, cx| {
- this.window_activation_changed(active, cx)
- }));
+ subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
Self {
@@ -164,184 +257,184 @@ impl CollabTitlebarItem {
project,
user_store,
client,
- user_menu: cx.add_view(|cx| {
- let view_id = cx.view_id();
- let mut menu = ContextMenu::new(view_id, cx);
- menu.set_position_mode(OverlayPositionMode::Local);
- menu
- }),
- branch_popover: None,
- project_popover: None,
+ // user_menu: cx.add_view(|cx| {
+ // let view_id = cx.view_id();
+ // let mut menu = ContextMenu::new(view_id, cx);
+ // menu.set_position_mode(OverlayPositionMode::Local);
+ // menu
+ // }),
+ // branch_popover: None,
+ // project_popover: None,
_subscriptions: subscriptions,
}
}
- fn collect_title_root_names(
- &self,
- theme: Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let project = self.project.read(cx);
-
- let (name, entry) = {
- let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
- let worktree = worktree.read(cx);
- (worktree.root_name(), worktree.root_git_entry())
- });
-
- names_and_branches.next().unwrap_or(("", None))
- };
-
- let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
- let branch_prepended = entry
- .as_ref()
- .and_then(RepositoryEntry::branch)
- .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
- let project_style = theme.titlebar.project_menu_button.clone();
- let git_style = theme.titlebar.git_menu_button.clone();
- let item_spacing = theme.titlebar.item_spacing;
-
- let mut ret = Flex::row();
-
- if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
- ret = ret.with_child(project_host)
- }
-
- ret = ret.with_child(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
- let style = project_style
- .in_state(self.project_popover.is_some())
- .style_for(mouse_state);
- enum RecentProjectsTooltip {}
- Label::new(name, style.text.clone())
- .contained()
- .with_style(style.container)
- .aligned()
- .left()
- .with_tooltip::<RecentProjectsTooltip>(
- 0,
- "Recent projects",
- Some(Box::new(recent_projects::OpenRecent)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any_named("title-project-name")
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, move |_, this, cx| {
- this.toggle_project_menu(&Default::default(), cx)
- })
- .on_click(MouseButton::Left, move |_, _, _| {}),
- )
- .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
- );
- if let Some(git_branch) = branch_prepended {
- ret = ret.with_child(
- Flex::row().with_child(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
- enum BranchPopoverTooltip {}
- let style = git_style
- .in_state(self.branch_popover.is_some())
- .style_for(mouse_state);
- Label::new(git_branch, style.text.clone())
- .contained()
- .with_style(style.container.clone())
- .with_margin_right(item_spacing)
- .aligned()
- .left()
- .with_tooltip::<BranchPopoverTooltip>(
- 0,
- "Recent branches",
- Some(Box::new(ToggleVcsMenu)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any_named("title-project-branch")
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, move |_, this, cx| {
- this.toggle_vcs_menu(&Default::default(), cx)
- })
- .on_click(MouseButton::Left, move |_, _, _| {}),
- )
- .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
- ),
- )
- }
- ret.into_any()
- }
-
- fn collect_project_host(
- &self,
- theme: Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- if ActiveCall::global(cx).read(cx).room().is_none() {
- return None;
- }
- let project = self.project.read(cx);
- let user_store = self.user_store.read(cx);
-
- if project.is_local() {
- return None;
- }
-
- let Some(host) = project.host() else {
- return None;
- };
- let (Some(host_user), Some(participant_index)) = (
- user_store.get_cached_user(host.user_id),
- user_store.participant_indices().get(&host.user_id),
- ) else {
- return None;
- };
-
- enum ProjectHost {}
- enum ProjectHostTooltip {}
-
- let host_style = theme.titlebar.project_host.clone();
- let selection_style = theme
- .editor
- .selection_style_for_room_participant(participant_index.0);
- let peer_id = host.peer_id.clone();
-
- Some(
- MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
- let mut host_style = host_style.style_for(mouse_state).clone();
- host_style.text.color = selection_style.cursor;
- Label::new(host_user.github_login.clone(), host_style.text)
- .contained()
- .with_style(host_style.container)
- .aligned()
- .left()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- if let Some(task) =
- workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
- {
- task.detach_and_log_err(cx);
- }
- }
- })
- .with_tooltip::<ProjectHostTooltip>(
- 0,
- host_user.github_login.clone() + " is sharing this project. Click to follow.",
- None,
- theme.tooltip.clone(),
- cx,
- )
- .into_any_named("project-host"),
- )
- }
-
- fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
- let project = if active {
+ // fn collect_title_root_names(
+ // &self,
+ // theme: Arc<Theme>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let project = self.project.read(cx);
+
+ // let (name, entry) = {
+ // let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+ // let worktree = worktree.read(cx);
+ // (worktree.root_name(), worktree.root_git_entry())
+ // });
+
+ // names_and_branches.next().unwrap_or(("", None))
+ // };
+
+ // let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
+ // let branch_prepended = entry
+ // .as_ref()
+ // .and_then(RepositoryEntry::branch)
+ // .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+ // let project_style = theme.titlebar.project_menu_button.clone();
+ // let git_style = theme.titlebar.git_menu_button.clone();
+ // let item_spacing = theme.titlebar.item_spacing;
+
+ // let mut ret = Flex::row();
+
+ // if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
+ // ret = ret.with_child(project_host)
+ // }
+
+ // ret = ret.with_child(
+ // Stack::new()
+ // .with_child(
+ // MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
+ // let style = project_style
+ // .in_state(self.project_popover.is_some())
+ // .style_for(mouse_state);
+ // enum RecentProjectsTooltip {}
+ // Label::new(name, style.text.clone())
+ // .contained()
+ // .with_style(style.container)
+ // .aligned()
+ // .left()
+ // .with_tooltip::<RecentProjectsTooltip>(
+ // 0,
+ // "Recent projects",
+ // Some(Box::new(recent_projects::OpenRecent)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .into_any_named("title-project-name")
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_down(MouseButton::Left, move |_, this, cx| {
+ // this.toggle_project_menu(&Default::default(), cx)
+ // })
+ // .on_click(MouseButton::Left, move |_, _, _| {}),
+ // )
+ // .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
+ // );
+ // if let Some(git_branch) = branch_prepended {
+ // ret = ret.with_child(
+ // Flex::row().with_child(
+ // Stack::new()
+ // .with_child(
+ // MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
+ // enum BranchPopoverTooltip {}
+ // let style = git_style
+ // .in_state(self.branch_popover.is_some())
+ // .style_for(mouse_state);
+ // Label::new(git_branch, style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // .with_margin_right(item_spacing)
+ // .aligned()
+ // .left()
+ // .with_tooltip::<BranchPopoverTooltip>(
+ // 0,
+ // "Recent branches",
+ // Some(Box::new(ToggleVcsMenu)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .into_any_named("title-project-branch")
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_down(MouseButton::Left, move |_, this, cx| {
+ // this.toggle_vcs_menu(&Default::default(), cx)
+ // })
+ // .on_click(MouseButton::Left, move |_, _, _| {}),
+ // )
+ // .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+ // ),
+ // )
+ // }
+ // ret.into_any()
+ // }
+
+ // fn collect_project_host(
+ // &self,
+ // theme: Arc<Theme>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> Option<AnyElement<Self>> {
+ // if ActiveCall::global(cx).read(cx).room().is_none() {
+ // return None;
+ // }
+ // let project = self.project.read(cx);
+ // let user_store = self.user_store.read(cx);
+
+ // if project.is_local() {
+ // return None;
+ // }
+
+ // let Some(host) = project.host() else {
+ // return None;
+ // };
+ // let (Some(host_user), Some(participant_index)) = (
+ // user_store.get_cached_user(host.user_id),
+ // user_store.participant_indices().get(&host.user_id),
+ // ) else {
+ // return None;
+ // };
+
+ // enum ProjectHost {}
+ // enum ProjectHostTooltip {}
+
+ // let host_style = theme.titlebar.project_host.clone();
+ // let selection_style = theme
+ // .editor
+ // .selection_style_for_room_participant(participant_index.0);
+ // let peer_id = host.peer_id.clone();
+
+ // Some(
+ // MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
+ // let mut host_style = host_style.style_for(mouse_state).clone();
+ // host_style.text.color = selection_style.cursor;
+ // Label::new(host_user.github_login.clone(), host_style.text)
+ // .contained()
+ // .with_style(host_style.container)
+ // .aligned()
+ // .left()
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if let Some(workspace) = this.workspace.upgrade(cx) {
+ // if let Some(task) =
+ // workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+ // {
+ // task.detach_and_log_err(cx);
+ // }
+ // }
+ // })
+ // .with_tooltip::<ProjectHostTooltip>(
+ // 0,
+ // host_user.github_login.clone() + " is sharing this project. Click to follow.",
+ // None,
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .into_any_named("project-host"),
+ // )
+ // }
+
+ fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
+ let project = if cx.is_window_active() {
Some(self.project.clone())
} else {
None
@@ -355,924 +448,924 @@ impl CollabTitlebarItem {
cx.notify();
}
- fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
- let active_call = ActiveCall::global(cx);
- let project = self.project.clone();
- active_call
- .update(cx, |call, cx| call.share_project(project, cx))
- .detach_and_log_err(cx);
- }
-
- fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
- let active_call = ActiveCall::global(cx);
- let project = self.project.clone();
- active_call
- .update(cx, |call, cx| call.unshare_project(project, cx))
- .log_err();
- }
-
- pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
- self.user_menu.update(cx, |user_menu, cx| {
- let items = if let Some(_) = self.user_store.read(cx).current_user() {
- vec![
- ContextMenuItem::action("Settings", zed_actions::OpenSettings),
- ContextMenuItem::action("Theme", theme_selector::Toggle),
- ContextMenuItem::separator(),
- ContextMenuItem::action(
- "Share Feedback",
- feedback::feedback_editor::GiveFeedback,
- ),
- ContextMenuItem::action("Sign Out", SignOut),
- ]
- } else {
- vec![
- ContextMenuItem::action("Settings", zed_actions::OpenSettings),
- ContextMenuItem::action("Theme", theme_selector::Toggle),
- ContextMenuItem::separator(),
- ContextMenuItem::action(
- "Share Feedback",
- feedback::feedback_editor::GiveFeedback,
- ),
- ]
- };
- user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
- });
- }
-
- fn render_branches_popover_host<'a>(
- &'a self,
- _theme: &'a theme::Titlebar,
- cx: &'a mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- self.branch_popover.as_ref().map(|child| {
- let theme = theme::current(cx).clone();
- let child = ChildView::new(child, cx);
- let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
- child
- .flex(1., true)
- .contained()
- .constrained()
- .with_width(theme.titlebar.menu.width)
- .with_height(theme.titlebar.menu.height)
- })
- .on_click(MouseButton::Left, |_, _, _| {})
- .on_down_out(MouseButton::Left, move |_, this, cx| {
- this.branch_popover.take();
- cx.emit(());
- cx.notify();
- })
- .contained()
- .into_any();
-
- Overlay::new(child)
- .with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::TopLeft)
- .with_z_index(999)
- .aligned()
- .bottom()
- .left()
- .into_any()
- })
- }
-
- fn render_project_popover_host<'a>(
- &'a self,
- _theme: &'a theme::Titlebar,
- cx: &'a mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- self.project_popover.as_ref().map(|child| {
- let theme = theme::current(cx).clone();
- let child = ChildView::new(child, cx);
- let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
- child
- .flex(1., true)
- .contained()
- .constrained()
- .with_width(theme.titlebar.menu.width)
- .with_height(theme.titlebar.menu.height)
- })
- .on_click(MouseButton::Left, |_, _, _| {})
- .on_down_out(MouseButton::Left, move |_, this, cx| {
- this.project_popover.take();
- cx.emit(());
- cx.notify();
- })
- .into_any();
-
- Overlay::new(child)
- .with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::TopLeft)
- .with_z_index(999)
- .aligned()
- .bottom()
- .left()
- .into_any()
- })
- }
-
- pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
- if self.branch_popover.take().is_none() {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- let Some(view) =
- cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
- else {
- return;
- };
- cx.subscribe(&view, |this, _, event, cx| {
- match event {
- PickerEvent::Dismiss => {
- this.branch_popover = None;
- }
- }
-
- cx.notify();
- })
- .detach();
- self.project_popover.take();
- cx.focus(&view);
- self.branch_popover = Some(view);
- }
- }
-
- cx.notify();
- }
-
- pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
- let workspace = self.workspace.clone();
- if self.project_popover.take().is_none() {
- cx.spawn(|this, mut cx| async move {
- let workspaces = WORKSPACE_DB
- .recent_workspaces_on_disk()
- .await
- .unwrap_or_default()
- .into_iter()
- .map(|(_, location)| location)
- .collect();
-
- let workspace = workspace.clone();
- this.update(&mut cx, move |this, cx| {
- let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
-
- cx.subscribe(&view, |this, _, event, cx| {
- match event {
- PickerEvent::Dismiss => {
- this.project_popover = None;
- }
- }
-
- cx.notify();
- })
- .detach();
- cx.focus(&view);
- this.branch_popover.take();
- this.project_popover = Some(view);
- cx.notify();
- })
- .log_err();
- })
- .detach();
- }
- cx.notify();
- }
-
- fn render_toggle_screen_sharing_button(
- &self,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let icon;
- let tooltip;
- if room.read(cx).is_screen_sharing() {
- icon = "icons/desktop.svg";
- tooltip = "Stop Sharing Screen"
- } else {
- icon = "icons/desktop.svg";
- tooltip = "Share Screen";
- }
-
- let active = room.read(cx).is_screen_sharing();
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
- let style = titlebar
- .screen_share_button
- .in_state(active)
- .style_for(state);
-
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- toggle_screen_sharing(&Default::default(), cx)
- })
- .with_tooltip::<ToggleScreenSharing>(
- 0,
- tooltip,
- Some(Box::new(ToggleScreenSharing)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_toggle_mute(
- &self,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let icon;
- let tooltip;
- let is_muted = room.read(cx).is_muted(cx);
- if is_muted {
- icon = "icons/mic-mute.svg";
- tooltip = "Unmute microphone";
- } else {
- icon = "icons/mic.svg";
- tooltip = "Mute microphone";
- }
-
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
- let style = titlebar
- .toggle_microphone_button
- .in_state(is_muted)
- .style_for(state);
- let image = Svg::new(icon)
- .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);
- if let Some(color) = style.container.background_color {
- image.with_background_color(color)
- } else {
- image
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- toggle_mute(&Default::default(), cx)
- })
- .with_tooltip::<ToggleMute>(
- 0,
- tooltip,
- Some(Box::new(ToggleMute)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_toggle_deafen(
- &self,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let icon;
- let tooltip;
- let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
- if is_deafened {
- icon = "icons/speaker-off.svg";
- tooltip = "Unmute speakers";
- } else {
- icon = "icons/speaker-loud.svg";
- tooltip = "Mute speakers";
- }
-
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
- let style = titlebar
- .toggle_speakers_button
- .in_state(is_deafened)
- .style_for(state);
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- toggle_deafen(&Default::default(), cx)
- })
- .with_tooltip::<ToggleDeafen>(
- 0,
- tooltip,
- Some(Box::new(ToggleDeafen)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let icon = "icons/exit.svg";
- let tooltip = "Leave call";
-
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
- let style = titlebar.leave_call_button.style_for(state);
- Svg::new(icon)
- .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)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- ActiveCall::global(cx)
- .update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
- })
- .with_tooltip::<LeaveCall>(
- 0,
- tooltip,
- Some(Box::new(LeaveCall)),
- theme.tooltip.clone(),
- cx,
- )
- .aligned()
- .into_any()
- }
- fn render_in_call_share_unshare_button(
- &self,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- let project = workspace.read(cx).project();
- if project.read(cx).is_remote() {
- return None;
- }
-
- let is_shared = project.read(cx).is_shared();
- let label = if is_shared { "Stop Sharing" } else { "Share" };
- let tooltip = if is_shared {
- "Stop sharing project with call participants"
- } else {
- "Share project with call participants"
- };
-
- let titlebar = &theme.titlebar;
-
- enum ShareUnshare {}
- Some(
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
- //TODO: Ensure this button has consistent width for both text variations
- let style = titlebar.share_button.inactive_state().style_for(state);
- Label::new(label, style.text.clone())
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if is_shared {
- this.unshare_project(&Default::default(), cx);
- } else {
- this.share_project(&Default::default(), cx);
- }
- })
- .with_tooltip::<ShareUnshare>(
- 0,
- tooltip.to_owned(),
- None,
- theme.tooltip.clone(),
- cx,
- ),
- )
- .aligned()
- .contained()
- .with_margin_left(theme.titlebar.item_spacing)
- .into_any(),
- )
- }
-
- fn render_user_menu_button(
- &self,
- theme: &Theme,
- avatar: Option<Arc<ImageData>>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let tooltip = theme.tooltip.clone();
- let user_menu_button_style = if avatar.is_some() {
- &theme.titlebar.user_menu.user_menu_button_online
- } else {
- &theme.titlebar.user_menu.user_menu_button_offline
- };
-
- let avatar_style = &user_menu_button_style.avatar;
- Stack::new()
- .with_child(
- MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
- let style = user_menu_button_style
- .user_menu
- .inactive_state()
- .style_for(state);
-
- let mut dropdown = Flex::row().align_children_center();
-
- if let Some(avatar_img) = avatar {
- dropdown = dropdown.with_child(Self::render_face(
- avatar_img,
- *avatar_style,
- Color::transparent_black(),
- None,
- ));
- };
-
- dropdown
- .with_child(
- Svg::new("icons/caret_down.svg")
- .with_color(user_menu_button_style.icon.color)
- .constrained()
- .with_width(user_menu_button_style.icon.width)
- .contained()
- .into_any(),
- )
- .aligned()
- .constrained()
- .with_height(style.width)
- .contained()
- .with_style(style.container)
- .into_any()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, move |_, this, cx| {
- this.user_menu.update(cx, |menu, _| menu.delay_cancel());
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.toggle_user_menu(&Default::default(), cx)
- })
- .with_tooltip::<ToggleUserMenu>(
- 0,
- "Toggle User Menu".to_owned(),
- Some(Box::new(ToggleUserMenu)),
- tooltip,
- cx,
- )
- .contained(),
- )
- .with_child(
- ChildView::new(&self.user_menu, cx)
- .aligned()
- .bottom()
- .right(),
- )
- .into_any()
- }
-
- fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let titlebar = &theme.titlebar;
- MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
- let style = titlebar.sign_in_button.inactive_state().style_for(state);
- Label::new("Sign In", style.text.clone())
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- let client = this.client.clone();
- cx.app_context()
- .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
- .detach_and_log_err(cx);
- })
- .into_any()
- }
-
- fn render_collaborators(
- &self,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- room: &ModelHandle<Room>,
- cx: &mut ViewContext<Self>,
- ) -> Vec<Container<Self>> {
- let mut participants = room
- .read(cx)
- .remote_participants()
- .values()
- .cloned()
- .collect::<Vec<_>>();
- participants.sort_by_cached_key(|p| p.user.github_login.clone());
-
- participants
- .into_iter()
- .filter_map(|participant| {
- let project = workspace.read(cx).project().read(cx);
- let replica_id = project
- .collaborators()
- .get(&participant.peer_id)
- .map(|collaborator| collaborator.replica_id);
- let user = participant.user.clone();
- Some(
- Container::new(self.render_face_pile(
- &user,
- replica_id,
- participant.peer_id,
- Some(participant.location),
- participant.muted,
- participant.speaking,
- workspace,
- theme,
- cx,
- ))
- .with_margin_right(theme.titlebar.face_pile_spacing),
- )
- })
- .collect()
- }
-
- fn render_current_user(
- &self,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- user: &Arc<User>,
- peer_id: PeerId,
- muted: bool,
- speaking: bool,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let replica_id = workspace.read(cx).project().read(cx).replica_id();
-
- Container::new(self.render_face_pile(
- user,
- Some(replica_id),
- peer_id,
- None,
- muted,
- speaking,
- workspace,
- theme,
- cx,
- ))
- .with_margin_right(theme.titlebar.item_spacing)
- .into_any()
- }
-
- fn render_face_pile(
- &self,
- user: &User,
- _replica_id: Option<ReplicaId>,
- peer_id: PeerId,
- location: Option<ParticipantLocation>,
- muted: bool,
- speaking: bool,
- workspace: &ViewHandle<Workspace>,
- theme: &Theme,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let user_id = user.id;
- let project_id = workspace.read(cx).project().read(cx).remote_id();
- let room = ActiveCall::global(cx).read(cx).room().cloned();
- let self_peer_id = workspace.read(cx).client().peer_id();
- let self_following = workspace.read(cx).is_being_followed(peer_id);
- let self_following_initialized = self_following
- && room.as_ref().map_or(false, |room| match project_id {
- None => true,
- Some(project_id) => room
- .read(cx)
- .followers_for(peer_id, project_id)
- .iter()
- .any(|&follower| Some(follower) == self_peer_id),
- });
-
- let leader_style = theme.titlebar.leader_avatar;
- let follower_style = theme.titlebar.follower_avatar;
-
- let microphone_state = if muted {
- Some(theme.titlebar.muted)
- } else if speaking {
- Some(theme.titlebar.speaking)
- } else {
- None
- };
-
- let mut background_color = theme
- .titlebar
- .container
- .background_color
- .unwrap_or_default();
-
- let participant_index = self
- .user_store
- .read(cx)
- .participant_indices()
- .get(&user_id)
- .copied();
- if let Some(participant_index) = participant_index {
- if self_following_initialized {
- let selection = theme
- .editor
- .selection_style_for_room_participant(participant_index.0)
- .selection;
- background_color = Color::blend(selection, background_color);
- background_color.a = 255;
- }
- }
-
- enum TitlebarParticipant {}
-
- let content = MouseEventHandler::new::<TitlebarParticipant, _>(
- peer_id.as_u64() as usize,
- cx,
- move |_, cx| {
- Stack::new()
- .with_children(user.avatar.as_ref().map(|avatar| {
- let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
- .with_child(Self::render_face(
- avatar.clone(),
- Self::location_style(workspace, location, leader_style, cx),
- background_color,
- microphone_state,
- ))
- .with_children(
- (|| {
- let project_id = project_id?;
- let room = room?.read(cx);
- let followers = room.followers_for(peer_id, project_id);
- Some(followers.into_iter().filter_map(|&follower| {
- if Some(follower) == self_peer_id {
- return None;
- }
- let participant =
- room.remote_participant_for_peer_id(follower)?;
- Some(Self::render_face(
- participant.user.avatar.clone()?,
- follower_style,
- background_color,
- None,
- ))
- }))
- })()
- .into_iter()
- .flatten(),
- )
- .with_children(
- self_following_initialized
- .then(|| self.user_store.read(cx).current_user())
- .and_then(|user| {
- Some(Self::render_face(
- user?.avatar.clone()?,
- follower_style,
- background_color,
- None,
- ))
- }),
- );
-
- let mut container = face_pile
- .contained()
- .with_style(theme.titlebar.leader_selection);
-
- if let Some(participant_index) = participant_index {
- if self_following_initialized {
- let color = theme
- .editor
- .selection_style_for_room_participant(participant_index.0)
- .selection;
- container = container.with_background_color(color);
- }
- }
-
- container
- }))
- .with_children((|| {
- let participant_index = participant_index?;
- let color = theme
- .editor
- .selection_style_for_room_participant(participant_index.0)
- .cursor;
- Some(
- AvatarRibbon::new(color)
- .constrained()
- .with_width(theme.titlebar.avatar_ribbon.width)
- .with_height(theme.titlebar.avatar_ribbon.height)
- .aligned()
- .bottom(),
- )
- })())
- },
- );
-
- if Some(peer_id) == self_peer_id {
- return content.into_any();
- }
-
- content
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- let Some(workspace) = this.workspace.upgrade(cx) else {
- return;
- };
- if let Some(task) =
- workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
- {
- task.detach_and_log_err(cx);
- }
- })
- .with_tooltip::<TitlebarParticipant>(
- peer_id.as_u64() as usize,
- format!("Follow {}", user.github_login),
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any()
- }
-
- fn location_style(
- workspace: &ViewHandle<Workspace>,
- location: Option<ParticipantLocation>,
- mut style: AvatarStyle,
- cx: &ViewContext<Self>,
- ) -> AvatarStyle {
- if let Some(location) = location {
- if let ParticipantLocation::SharedProject { project_id } = location {
- if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
- style.image.grayscale = true;
- }
- } else {
- style.image.grayscale = true;
- }
- }
-
- style
- }
-
- fn render_face<V: 'static>(
- avatar: Arc<ImageData>,
- avatar_style: AvatarStyle,
- background_color: Color,
- microphone_state: Option<Color>,
- ) -> AnyElement<V> {
- Image::from_data(avatar)
- .with_style(avatar_style.image)
- .aligned()
- .contained()
- .with_background_color(microphone_state.unwrap_or(background_color))
- .with_corner_radius(avatar_style.outer_corner_radius)
- .constrained()
- .with_width(avatar_style.outer_width)
- .with_height(avatar_style.outer_width)
- .aligned()
- .into_any()
- }
-
- fn render_connection_status(
- &self,
- status: &client::Status,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- enum ConnectionStatusButton {}
-
- let theme = &theme::current(cx).clone();
- match status {
- client::Status::ConnectionError
- | client::Status::ConnectionLost
- | client::Status::Reauthenticating { .. }
- | client::Status::Reconnecting { .. }
- | client::Status::ReconnectionError { .. } => Some(
- Svg::new("icons/disconnected.svg")
- .with_color(theme.titlebar.offline_icon.color)
- .constrained()
- .with_width(theme.titlebar.offline_icon.width)
- .aligned()
- .contained()
- .with_style(theme.titlebar.offline_icon.container)
- .into_any(),
- ),
- client::Status::UpgradeRequired => {
- let auto_updater = auto_update::AutoUpdater::get(cx);
- let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
- Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
- Some(AutoUpdateStatus::Installing)
- | Some(AutoUpdateStatus::Downloading)
- | Some(AutoUpdateStatus::Checking) => "Updating...",
- Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
- "Please update Zed to Collaborate"
- }
- };
-
- Some(
- MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
- Label::new(label, theme.titlebar.outdated_warning.text.clone())
- .contained()
- .with_style(theme.titlebar.outdated_warning.container)
- .aligned()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, |_, _, cx| {
- if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
- if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
- workspace::restart(&Default::default(), cx);
- return;
- }
- }
- auto_update::check(&Default::default(), cx);
- })
- .into_any(),
- )
- }
- _ => None,
- }
- }
-}
-
-pub struct AvatarRibbon {
- color: Color,
+ // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
+ // let active_call = ActiveCall::global(cx);
+ // let project = self.project.clone();
+ // active_call
+ // .update(cx, |call, cx| call.share_project(project, cx))
+ // .detach_and_log_err(cx);
+ // }
+
+ // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+ // let active_call = ActiveCall::global(cx);
+ // let project = self.project.clone();
+ // active_call
+ // .update(cx, |call, cx| call.unshare_project(project, cx))
+ // .log_err();
+ // }
+
+ // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
+ // self.user_menu.update(cx, |user_menu, cx| {
+ // let items = if let Some(_) = self.user_store.read(cx).current_user() {
+ // vec![
+ // ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+ // ContextMenuItem::action("Theme", theme_selector::Toggle),
+ // ContextMenuItem::separator(),
+ // ContextMenuItem::action(
+ // "Share Feedback",
+ // feedback::feedback_editor::GiveFeedback,
+ // ),
+ // ContextMenuItem::action("Sign Out", SignOut),
+ // ]
+ // } else {
+ // vec![
+ // ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+ // ContextMenuItem::action("Theme", theme_selector::Toggle),
+ // ContextMenuItem::separator(),
+ // ContextMenuItem::action(
+ // "Share Feedback",
+ // feedback::feedback_editor::GiveFeedback,
+ // ),
+ // ]
+ // };
+ // user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
+ // });
+ // }
+
+ // fn render_branches_popover_host<'a>(
+ // &'a self,
+ // _theme: &'a theme::Titlebar,
+ // cx: &'a mut ViewContext<Self>,
+ // ) -> Option<AnyElement<Self>> {
+ // self.branch_popover.as_ref().map(|child| {
+ // let theme = theme::current(cx).clone();
+ // let child = ChildView::new(child, cx);
+ // let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
+ // child
+ // .flex(1., true)
+ // .contained()
+ // .constrained()
+ // .with_width(theme.titlebar.menu.width)
+ // .with_height(theme.titlebar.menu.height)
+ // })
+ // .on_click(MouseButton::Left, |_, _, _| {})
+ // .on_down_out(MouseButton::Left, move |_, this, cx| {
+ // this.branch_popover.take();
+ // cx.emit(());
+ // cx.notify();
+ // })
+ // .contained()
+ // .into_any();
+
+ // Overlay::new(child)
+ // .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ // .with_anchor_corner(AnchorCorner::TopLeft)
+ // .with_z_index(999)
+ // .aligned()
+ // .bottom()
+ // .left()
+ // .into_any()
+ // })
+ // }
+
+ // fn render_project_popover_host<'a>(
+ // &'a self,
+ // _theme: &'a theme::Titlebar,
+ // cx: &'a mut ViewContext<Self>,
+ // ) -> Option<AnyElement<Self>> {
+ // self.project_popover.as_ref().map(|child| {
+ // let theme = theme::current(cx).clone();
+ // let child = ChildView::new(child, cx);
+ // let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
+ // child
+ // .flex(1., true)
+ // .contained()
+ // .constrained()
+ // .with_width(theme.titlebar.menu.width)
+ // .with_height(theme.titlebar.menu.height)
+ // })
+ // .on_click(MouseButton::Left, |_, _, _| {})
+ // .on_down_out(MouseButton::Left, move |_, this, cx| {
+ // this.project_popover.take();
+ // cx.emit(());
+ // cx.notify();
+ // })
+ // .into_any();
+
+ // Overlay::new(child)
+ // .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ // .with_anchor_corner(AnchorCorner::TopLeft)
+ // .with_z_index(999)
+ // .aligned()
+ // .bottom()
+ // .left()
+ // .into_any()
+ // })
+ // }
+
+ // pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+ // if self.branch_popover.take().is_none() {
+ // if let Some(workspace) = self.workspace.upgrade(cx) {
+ // let Some(view) =
+ // cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
+ // else {
+ // return;
+ // };
+ // cx.subscribe(&view, |this, _, event, cx| {
+ // match event {
+ // PickerEvent::Dismiss => {
+ // this.branch_popover = None;
+ // }
+ // }
+
+ // cx.notify();
+ // })
+ // .detach();
+ // self.project_popover.take();
+ // cx.focus(&view);
+ // self.branch_popover = Some(view);
+ // }
+ // }
+
+ // cx.notify();
+ // }
+
+ // pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+ // let workspace = self.workspace.clone();
+ // if self.project_popover.take().is_none() {
+ // cx.spawn(|this, mut cx| async move {
+ // let workspaces = WORKSPACE_DB
+ // .recent_workspaces_on_disk()
+ // .await
+ // .unwrap_or_default()
+ // .into_iter()
+ // .map(|(_, location)| location)
+ // .collect();
+
+ // let workspace = workspace.clone();
+ // this.update(&mut cx, move |this, cx| {
+ // let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+ // cx.subscribe(&view, |this, _, event, cx| {
+ // match event {
+ // PickerEvent::Dismiss => {
+ // this.project_popover = None;
+ // }
+ // }
+
+ // cx.notify();
+ // })
+ // .detach();
+ // cx.focus(&view);
+ // this.branch_popover.take();
+ // this.project_popover = Some(view);
+ // cx.notify();
+ // })
+ // .log_err();
+ // })
+ // .detach();
+ // }
+ // cx.notify();
+ // }
+
+ // fn render_toggle_screen_sharing_button(
+ // &self,
+ // theme: &Theme,
+ // room: &ModelHandle<Room>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let icon;
+ // let tooltip;
+ // if room.read(cx).is_screen_sharing() {
+ // icon = "icons/desktop.svg";
+ // tooltip = "Stop Sharing Screen"
+ // } else {
+ // icon = "icons/desktop.svg";
+ // tooltip = "Share Screen";
+ // }
+
+ // let active = room.read(cx).is_screen_sharing();
+ // let titlebar = &theme.titlebar;
+ // MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
+ // let style = titlebar
+ // .screen_share_button
+ // .in_state(active)
+ // .style_for(state);
+
+ // Svg::new(icon)
+ // .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)
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, _, cx| {
+ // toggle_screen_sharing(&Default::default(), cx)
+ // })
+ // .with_tooltip::<ToggleScreenSharing>(
+ // 0,
+ // tooltip,
+ // Some(Box::new(ToggleScreenSharing)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .aligned()
+ // .into_any()
+ // }
+ // fn render_toggle_mute(
+ // &self,
+ // theme: &Theme,
+ // room: &ModelHandle<Room>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let icon;
+ // let tooltip;
+ // let is_muted = room.read(cx).is_muted(cx);
+ // if is_muted {
+ // icon = "icons/mic-mute.svg";
+ // tooltip = "Unmute microphone";
+ // } else {
+ // icon = "icons/mic.svg";
+ // tooltip = "Mute microphone";
+ // }
+
+ // let titlebar = &theme.titlebar;
+ // MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
+ // let style = titlebar
+ // .toggle_microphone_button
+ // .in_state(is_muted)
+ // .style_for(state);
+ // let image = Svg::new(icon)
+ // .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);
+ // if let Some(color) = style.container.background_color {
+ // image.with_background_color(color)
+ // } else {
+ // image
+ // }
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, _, cx| {
+ // toggle_mute(&Default::default(), cx)
+ // })
+ // .with_tooltip::<ToggleMute>(
+ // 0,
+ // tooltip,
+ // Some(Box::new(ToggleMute)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .aligned()
+ // .into_any()
+ // }
+ // fn render_toggle_deafen(
+ // &self,
+ // theme: &Theme,
+ // room: &ModelHandle<Room>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let icon;
+ // let tooltip;
+ // let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
+ // if is_deafened {
+ // icon = "icons/speaker-off.svg";
+ // tooltip = "Unmute speakers";
+ // } else {
+ // icon = "icons/speaker-loud.svg";
+ // tooltip = "Mute speakers";
+ // }
+
+ // let titlebar = &theme.titlebar;
+ // MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
+ // let style = titlebar
+ // .toggle_speakers_button
+ // .in_state(is_deafened)
+ // .style_for(state);
+ // Svg::new(icon)
+ // .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)
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, _, cx| {
+ // toggle_deafen(&Default::default(), cx)
+ // })
+ // .with_tooltip::<ToggleDeafen>(
+ // 0,
+ // tooltip,
+ // Some(Box::new(ToggleDeafen)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .aligned()
+ // .into_any()
+ // }
+ // fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ // let icon = "icons/exit.svg";
+ // let tooltip = "Leave call";
+
+ // let titlebar = &theme.titlebar;
+ // MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
+ // let style = titlebar.leave_call_button.style_for(state);
+ // Svg::new(icon)
+ // .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)
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, _, cx| {
+ // ActiveCall::global(cx)
+ // .update(cx, |call, cx| call.hang_up(cx))
+ // .detach_and_log_err(cx);
+ // })
+ // .with_tooltip::<LeaveCall>(
+ // 0,
+ // tooltip,
+ // Some(Box::new(LeaveCall)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .aligned()
+ // .into_any()
+ // }
+ // fn render_in_call_share_unshare_button(
+ // &self,
+ // workspace: &ViewHandle<Workspace>,
+ // theme: &Theme,
+ // cx: &mut ViewContext<Self>,
+ // ) -> Option<AnyElement<Self>> {
+ // let project = workspace.read(cx).project();
+ // if project.read(cx).is_remote() {
+ // return None;
+ // }
+
+ // let is_shared = project.read(cx).is_shared();
+ // let label = if is_shared { "Stop Sharing" } else { "Share" };
+ // let tooltip = if is_shared {
+ // "Stop sharing project with call participants"
+ // } else {
+ // "Share project with call participants"
+ // };
+
+ // let titlebar = &theme.titlebar;
+
+ // enum ShareUnshare {}
+ // Some(
+ // Stack::new()
+ // .with_child(
+ // MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
+ // //TODO: Ensure this button has consistent width for both text variations
+ // let style = titlebar.share_button.inactive_state().style_for(state);
+ // Label::new(label, style.text.clone())
+ // .contained()
+ // .with_style(style.container)
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if is_shared {
+ // this.unshare_project(&Default::default(), cx);
+ // } else {
+ // this.share_project(&Default::default(), cx);
+ // }
+ // })
+ // .with_tooltip::<ShareUnshare>(
+ // 0,
+ // tooltip.to_owned(),
+ // None,
+ // theme.tooltip.clone(),
+ // cx,
+ // ),
+ // )
+ // .aligned()
+ // .contained()
+ // .with_margin_left(theme.titlebar.item_spacing)
+ // .into_any(),
+ // )
+ // }
+
+ // fn render_user_menu_button(
+ // &self,
+ // theme: &Theme,
+ // avatar: Option<Arc<ImageData>>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let tooltip = theme.tooltip.clone();
+ // let user_menu_button_style = if avatar.is_some() {
+ // &theme.titlebar.user_menu.user_menu_button_online
+ // } else {
+ // &theme.titlebar.user_menu.user_menu_button_offline
+ // };
+
+ // let avatar_style = &user_menu_button_style.avatar;
+ // Stack::new()
+ // .with_child(
+ // MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
+ // let style = user_menu_button_style
+ // .user_menu
+ // .inactive_state()
+ // .style_for(state);
+
+ // let mut dropdown = Flex::row().align_children_center();
+
+ // if let Some(avatar_img) = avatar {
+ // dropdown = dropdown.with_child(Self::render_face(
+ // avatar_img,
+ // *avatar_style,
+ // Color::transparent_black(),
+ // None,
+ // ));
+ // };
+
+ // dropdown
+ // .with_child(
+ // Svg::new("icons/caret_down.svg")
+ // .with_color(user_menu_button_style.icon.color)
+ // .constrained()
+ // .with_width(user_menu_button_style.icon.width)
+ // .contained()
+ // .into_any(),
+ // )
+ // .aligned()
+ // .constrained()
+ // .with_height(style.width)
+ // .contained()
+ // .with_style(style.container)
+ // .into_any()
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_down(MouseButton::Left, move |_, this, cx| {
+ // this.user_menu.update(cx, |menu, _| menu.delay_cancel());
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.toggle_user_menu(&Default::default(), cx)
+ // })
+ // .with_tooltip::<ToggleUserMenu>(
+ // 0,
+ // "Toggle User Menu".to_owned(),
+ // Some(Box::new(ToggleUserMenu)),
+ // tooltip,
+ // cx,
+ // )
+ // .contained(),
+ // )
+ // .with_child(
+ // ChildView::new(&self.user_menu, cx)
+ // .aligned()
+ // .bottom()
+ // .right(),
+ // )
+ // .into_any()
+ // }
+
+ // fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ // let titlebar = &theme.titlebar;
+ // MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
+ // let style = titlebar.sign_in_button.inactive_state().style_for(state);
+ // Label::new("Sign In", style.text.clone())
+ // .contained()
+ // .with_style(style.container)
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // let client = this.client.clone();
+ // cx.app_context()
+ // .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
+ // .detach_and_log_err(cx);
+ // })
+ // .into_any()
+ // }
+
+ // fn render_collaborators(
+ // &self,
+ // workspace: &ViewHandle<Workspace>,
+ // theme: &Theme,
+ // room: &ModelHandle<Room>,
+ // cx: &mut ViewContext<Self>,
+ // ) -> Vec<Container<Self>> {
+ // let mut participants = room
+ // .read(cx)
+ // .remote_participants()
+ // .values()
+ // .cloned()
+ // .collect::<Vec<_>>();
+ // participants.sort_by_cached_key(|p| p.user.github_login.clone());
+
+ // participants
+ // .into_iter()
+ // .filter_map(|participant| {
+ // let project = workspace.read(cx).project().read(cx);
+ // let replica_id = project
+ // .collaborators()
+ // .get(&participant.peer_id)
+ // .map(|collaborator| collaborator.replica_id);
+ // let user = participant.user.clone();
+ // Some(
+ // Container::new(self.render_face_pile(
+ // &user,
+ // replica_id,
+ // participant.peer_id,
+ // Some(participant.location),
+ // participant.muted,
+ // participant.speaking,
+ // workspace,
+ // theme,
+ // cx,
+ // ))
+ // .with_margin_right(theme.titlebar.face_pile_spacing),
+ // )
+ // })
+ // .collect()
+ // }
+
+ // fn render_current_user(
+ // &self,
+ // workspace: &ViewHandle<Workspace>,
+ // theme: &Theme,
+ // user: &Arc<User>,
+ // peer_id: PeerId,
+ // muted: bool,
+ // speaking: bool,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let replica_id = workspace.read(cx).project().read(cx).replica_id();
+
+ // Container::new(self.render_face_pile(
+ // user,
+ // Some(replica_id),
+ // peer_id,
+ // None,
+ // muted,
+ // speaking,
+ // workspace,
+ // theme,
+ // cx,
+ // ))
+ // .with_margin_right(theme.titlebar.item_spacing)
+ // .into_any()
+ // }
+
+ // fn render_face_pile(
+ // &self,
+ // user: &User,
+ // _replica_id: Option<ReplicaId>,
+ // peer_id: PeerId,
+ // location: Option<ParticipantLocation>,
+ // muted: bool,
+ // speaking: bool,
+ // workspace: &ViewHandle<Workspace>,
+ // theme: &Theme,
+ // cx: &mut ViewContext<Self>,
+ // ) -> AnyElement<Self> {
+ // let user_id = user.id;
+ // let project_id = workspace.read(cx).project().read(cx).remote_id();
+ // let room = ActiveCall::global(cx).read(cx).room().cloned();
+ // let self_peer_id = workspace.read(cx).client().peer_id();
+ // let self_following = workspace.read(cx).is_being_followed(peer_id);
+ // let self_following_initialized = self_following
+ // && room.as_ref().map_or(false, |room| match project_id {
+ // None => true,
+ // Some(project_id) => room
+ // .read(cx)
+ // .followers_for(peer_id, project_id)
+ // .iter()
+ // .any(|&follower| Some(follower) == self_peer_id),
+ // });
+
+ // let leader_style = theme.titlebar.leader_avatar;
+ // let follower_style = theme.titlebar.follower_avatar;
+
+ // let microphone_state = if muted {
+ // Some(theme.titlebar.muted)
+ // } else if speaking {
+ // Some(theme.titlebar.speaking)
+ // } else {
+ // None
+ // };
+
+ // let mut background_color = theme
+ // .titlebar
+ // .container
+ // .background_color
+ // .unwrap_or_default();
+
+ // let participant_index = self
+ // .user_store
+ // .read(cx)
+ // .participant_indices()
+ // .get(&user_id)
+ // .copied();
+ // if let Some(participant_index) = participant_index {
+ // if self_following_initialized {
+ // let selection = theme
+ // .editor
+ // .selection_style_for_room_participant(participant_index.0)
+ // .selection;
+ // background_color = Color::blend(selection, background_color);
+ // background_color.a = 255;
+ // }
+ // }
+
+ // enum TitlebarParticipant {}
+
+ // let content = MouseEventHandler::new::<TitlebarParticipant, _>(
+ // peer_id.as_u64() as usize,
+ // cx,
+ // move |_, cx| {
+ // Stack::new()
+ // .with_children(user.avatar.as_ref().map(|avatar| {
+ // let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
+ // .with_child(Self::render_face(
+ // avatar.clone(),
+ // Self::location_style(workspace, location, leader_style, cx),
+ // background_color,
+ // microphone_state,
+ // ))
+ // .with_children(
+ // (|| {
+ // let project_id = project_id?;
+ // let room = room?.read(cx);
+ // let followers = room.followers_for(peer_id, project_id);
+ // Some(followers.into_iter().filter_map(|&follower| {
+ // if Some(follower) == self_peer_id {
+ // return None;
+ // }
+ // let participant =
+ // room.remote_participant_for_peer_id(follower)?;
+ // Some(Self::render_face(
+ // participant.user.avatar.clone()?,
+ // follower_style,
+ // background_color,
+ // None,
+ // ))
+ // }))
+ // })()
+ // .into_iter()
+ // .flatten(),
+ // )
+ // .with_children(
+ // self_following_initialized
+ // .then(|| self.user_store.read(cx).current_user())
+ // .and_then(|user| {
+ // Some(Self::render_face(
+ // user?.avatar.clone()?,
+ // follower_style,
+ // background_color,
+ // None,
+ // ))
+ // }),
+ // );
+
+ // let mut container = face_pile
+ // .contained()
+ // .with_style(theme.titlebar.leader_selection);
+
+ // if let Some(participant_index) = participant_index {
+ // if self_following_initialized {
+ // let color = theme
+ // .editor
+ // .selection_style_for_room_participant(participant_index.0)
+ // .selection;
+ // container = container.with_background_color(color);
+ // }
+ // }
+
+ // container
+ // }))
+ // .with_children((|| {
+ // let participant_index = participant_index?;
+ // let color = theme
+ // .editor
+ // .selection_style_for_room_participant(participant_index.0)
+ // .cursor;
+ // Some(
+ // AvatarRibbon::new(color)
+ // .constrained()
+ // .with_width(theme.titlebar.avatar_ribbon.width)
+ // .with_height(theme.titlebar.avatar_ribbon.height)
+ // .aligned()
+ // .bottom(),
+ // )
+ // })())
+ // },
+ // );
+
+ // if Some(peer_id) == self_peer_id {
+ // return content.into_any();
+ // }
+
+ // content
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // let Some(workspace) = this.workspace.upgrade(cx) else {
+ // return;
+ // };
+ // if let Some(task) =
+ // workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+ // {
+ // task.detach_and_log_err(cx);
+ // }
+ // })
+ // .with_tooltip::<TitlebarParticipant>(
+ // peer_id.as_u64() as usize,
+ // format!("Follow {}", user.github_login),
+ // Some(Box::new(FollowNextCollaborator)),
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .into_any()
+ // }
+
+ // fn location_style(
+ // workspace: &ViewHandle<Workspace>,
+ // location: Option<ParticipantLocation>,
+ // mut style: AvatarStyle,
+ // cx: &ViewContext<Self>,
+ // ) -> AvatarStyle {
+ // if let Some(location) = location {
+ // if let ParticipantLocation::SharedProject { project_id } = location {
+ // if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
+ // style.image.grayscale = true;
+ // }
+ // } else {
+ // style.image.grayscale = true;
+ // }
+ // }
+
+ // style
+ // }
+
+ // fn render_face<V: 'static>(
+ // avatar: Arc<ImageData>,
+ // avatar_style: AvatarStyle,
+ // background_color: Color,
+ // microphone_state: Option<Color>,
+ // ) -> AnyElement<V> {
+ // Image::from_data(avatar)
+ // .with_style(avatar_style.image)
+ // .aligned()
+ // .contained()
+ // .with_background_color(microphone_state.unwrap_or(background_color))
+ // .with_corner_radius(avatar_style.outer_corner_radius)
+ // .constrained()
+ // .with_width(avatar_style.outer_width)
+ // .with_height(avatar_style.outer_width)
+ // .aligned()
+ // .into_any()
+ // }
+
+ // fn render_connection_status(
+ // &self,
+ // status: &client::Status,
+ // cx: &mut ViewContext<Self>,
+ // ) -> Option<AnyElement<Self>> {
+ // enum ConnectionStatusButton {}
+
+ // let theme = &theme::current(cx).clone();
+ // match status {
+ // client::Status::ConnectionError
+ // | client::Status::ConnectionLost
+ // | client::Status::Reauthenticating { .. }
+ // | client::Status::Reconnecting { .. }
+ // | client::Status::ReconnectionError { .. } => Some(
+ // Svg::new("icons/disconnected.svg")
+ // .with_color(theme.titlebar.offline_icon.color)
+ // .constrained()
+ // .with_width(theme.titlebar.offline_icon.width)
+ // .aligned()
+ // .contained()
+ // .with_style(theme.titlebar.offline_icon.container)
+ // .into_any(),
+ // ),
+ // client::Status::UpgradeRequired => {
+ // let auto_updater = auto_update::AutoUpdater::get(cx);
+ // let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
+ // Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
+ // Some(AutoUpdateStatus::Installing)
+ // | Some(AutoUpdateStatus::Downloading)
+ // | Some(AutoUpdateStatus::Checking) => "Updating...",
+ // Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
+ // "Please update Zed to Collaborate"
+ // }
+ // };
+
+ // Some(
+ // MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
+ // Label::new(label, theme.titlebar.outdated_warning.text.clone())
+ // .contained()
+ // .with_style(theme.titlebar.outdated_warning.container)
+ // .aligned()
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, |_, _, cx| {
+ // if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
+ // if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
+ // workspace::restart(&Default::default(), cx);
+ // return;
+ // }
+ // }
+ // auto_update::check(&Default::default(), cx);
+ // })
+ // .into_any(),
+ // )
+ // }
+ // _ => None,
+ // }
+ // }
}
-impl AvatarRibbon {
- pub fn new(color: Color) -> AvatarRibbon {
- AvatarRibbon { color }
- }
-}
-
-impl Element<CollabTitlebarItem> for AvatarRibbon {
- type LayoutState = ();
-
- type PaintState = ();
-
- fn layout(
- &mut self,
- constraint: gpui::SizeConstraint,
- _: &mut CollabTitlebarItem,
- _: &mut ViewContext<CollabTitlebarItem>,
- ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
- (constraint.max, ())
- }
-
- fn paint(
- &mut self,
- bounds: RectF,
- _: RectF,
- _: &mut Self::LayoutState,
- _: &mut CollabTitlebarItem,
- cx: &mut ViewContext<CollabTitlebarItem>,
- ) -> Self::PaintState {
- let mut path = PathBuilder::new();
- path.reset(bounds.lower_left());
- path.curve_to(
- bounds.origin() + vec2f(bounds.height(), 0.),
- bounds.origin(),
- );
- path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
- path.curve_to(bounds.lower_right(), bounds.upper_right());
- path.line_to(bounds.lower_left());
- cx.scene().push_path(path.build(self.color, None));
- }
-
- fn rect_for_text_range(
- &self,
- _: Range<usize>,
- _: RectF,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &CollabTitlebarItem,
- _: &ViewContext<CollabTitlebarItem>,
- ) -> Option<RectF> {
- None
- }
-
- fn debug(
- &self,
- bounds: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &CollabTitlebarItem,
- _: &ViewContext<CollabTitlebarItem>,
- ) -> gpui::json::Value {
- json::json!({
- "type": "AvatarRibbon",
- "bounds": bounds.to_json(),
- "color": self.color.to_json(),
- })
- }
-}
+// pub struct AvatarRibbon {
+// color: Color,
+// }
+
+// impl AvatarRibbon {
+// pub fn new(color: Color) -> AvatarRibbon {
+// AvatarRibbon { color }
+// }
+// }
+
+// impl Element<CollabTitlebarItem> for AvatarRibbon {
+// type LayoutState = ();
+
+// type PaintState = ();
+
+// fn layout(
+// &mut self,
+// constraint: gpui::SizeConstraint,
+// _: &mut CollabTitlebarItem,
+// _: &mut ViewContext<CollabTitlebarItem>,
+// ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+// (constraint.max, ())
+// }
+
+// fn paint(
+// &mut self,
+// bounds: RectF,
+// _: RectF,
+// _: &mut Self::LayoutState,
+// _: &mut CollabTitlebarItem,
+// cx: &mut ViewContext<CollabTitlebarItem>,
+// ) -> Self::PaintState {
+// let mut path = PathBuilder::new();
+// path.reset(bounds.lower_left());
+// path.curve_to(
+// bounds.origin() + vec2f(bounds.height(), 0.),
+// bounds.origin(),
+// );
+// path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
+// path.curve_to(bounds.lower_right(), bounds.upper_right());
+// path.line_to(bounds.lower_left());
+// cx.scene().push_path(path.build(self.color, None));
+// }
+
+// fn rect_for_text_range(
+// &self,
+// _: Range<usize>,
+// _: RectF,
+// _: RectF,
+// _: &Self::LayoutState,
+// _: &Self::PaintState,
+// _: &CollabTitlebarItem,
+// _: &ViewContext<CollabTitlebarItem>,
+// ) -> Option<RectF> {
+// None
+// }
+
+// fn debug(
+// &self,
+// bounds: RectF,
+// _: &Self::LayoutState,
+// _: &Self::PaintState,
+// _: &CollabTitlebarItem,
+// _: &ViewContext<CollabTitlebarItem>,
+// ) -> gpui::json::Value {
+// json::json!({
+// "type": "AvatarRibbon",
+// "bounds": bounds.to_json(),
+// "color": self.color.to_json(),
+// })
+// }
+// }
@@ -7,159 +7,147 @@ pub mod notification_panel;
pub mod notifications;
mod panel_settings;
-use call::{report_call_event_for_room, ActiveCall, Room};
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
-use gpui::{
- actions,
- elements::{ContainerStyle, Empty, Image},
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- platform::{Screen, WindowBounds, WindowKind, WindowOptions},
- AnyElement, AppContext, Element, ImageData, Task,
-};
-use std::{rc::Rc, sync::Arc};
-use theme::AvatarStyle;
-use util::ResultExt;
-use workspace::AppState;
+use std::sync::Arc;
pub use collab_titlebar_item::CollabTitlebarItem;
+use gpui::AppContext;
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
+use settings::Settings;
+use workspace::AppState;
-actions!(
- collab,
- [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-);
+// actions!(
+// collab,
+// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
+// );
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
- settings::register::<CollaborationPanelSettings>(cx);
- settings::register::<ChatPanelSettings>(cx);
- settings::register::<NotificationPanelSettings>(cx);
+pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
+ CollaborationPanelSettings::register(cx);
+ ChatPanelSettings::register(cx);
+ NotificationPanelSettings::register(cx);
- vcs_menu::init(cx);
+ // vcs_menu::init(cx);
collab_titlebar_item::init(cx);
- collab_panel::init(cx);
- chat_panel::init(cx);
- notifications::init(&app_state, cx);
+ // collab_panel::init(cx);
+ // chat_panel::init(cx);
+ // notifications::init(&app_state, cx);
- cx.add_global_action(toggle_screen_sharing);
- cx.add_global_action(toggle_mute);
- cx.add_global_action(toggle_deafen);
+ // cx.add_global_action(toggle_screen_sharing);
+ // cx.add_global_action(toggle_mute);
+ // cx.add_global_action(toggle_deafen);
}
-pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
- let call = ActiveCall::global(cx).read(cx);
- if let Some(room) = call.room().cloned() {
- let client = call.client();
- let toggle_screen_sharing = room.update(cx, |room, cx| {
- if room.is_screen_sharing() {
- report_call_event_for_room(
- "disable screen share",
- room.id(),
- room.channel_id(),
- &client,
- cx,
- );
- Task::ready(room.unshare_screen(cx))
- } else {
- report_call_event_for_room(
- "enable screen share",
- room.id(),
- room.channel_id(),
- &client,
- cx,
- );
- room.share_screen(cx)
- }
- });
- toggle_screen_sharing.detach_and_log_err(cx);
- }
-}
+// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
+// let call = ActiveCall::global(cx).read(cx);
+// if let Some(room) = call.room().cloned() {
+// let client = call.client();
+// let toggle_screen_sharing = room.update(cx, |room, cx| {
+// if room.is_screen_sharing() {
+// report_call_event_for_room(
+// "disable screen share",
+// room.id(),
+// room.channel_id(),
+// &client,
+// cx,
+// );
+// Task::ready(room.unshare_screen(cx))
+// } else {
+// report_call_event_for_room(
+// "enable screen share",
+// room.id(),
+// room.channel_id(),
+// &client,
+// cx,
+// );
+// room.share_screen(cx)
+// }
+// });
+// toggle_screen_sharing.detach_and_log_err(cx);
+// }
+// }
-pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
- let call = ActiveCall::global(cx).read(cx);
- if let Some(room) = call.room().cloned() {
- let client = call.client();
- room.update(cx, |room, cx| {
- let operation = if room.is_muted(cx) {
- "enable microphone"
- } else {
- "disable microphone"
- };
- report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
+// let call = ActiveCall::global(cx).read(cx);
+// if let Some(room) = call.room().cloned() {
+// let client = call.client();
+// room.update(cx, |room, cx| {
+// let operation = if room.is_muted(cx) {
+// "enable microphone"
+// } else {
+// "disable microphone"
+// };
+// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
- room.toggle_mute(cx)
- })
- .map(|task| task.detach_and_log_err(cx))
- .log_err();
- }
-}
+// room.toggle_mute(cx)
+// })
+// .map(|task| task.detach_and_log_err(cx))
+// .log_err();
+// }
+// }
-pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
- if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
- room.update(cx, Room::toggle_deafen)
- .map(|task| task.detach_and_log_err(cx))
- .log_err();
- }
-}
+// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+// room.update(cx, Room::toggle_deafen)
+// .map(|task| task.detach_and_log_err(cx))
+// .log_err();
+// }
+// }
-fn notification_window_options(
- screen: Rc<dyn Screen>,
- window_size: Vector2F,
-) -> WindowOptions<'static> {
- const NOTIFICATION_PADDING: f32 = 16.;
+// fn notification_window_options(
+// screen: Rc<dyn Screen>,
+// window_size: Vector2F,
+// ) -> WindowOptions<'static> {
+// const NOTIFICATION_PADDING: f32 = 16.;
- let screen_bounds = screen.content_bounds();
- WindowOptions {
- bounds: WindowBounds::Fixed(RectF::new(
- screen_bounds.upper_right()
- + vec2f(
- -NOTIFICATION_PADDING - window_size.x(),
- NOTIFICATION_PADDING,
- ),
- window_size,
- )),
- titlebar: None,
- center: false,
- focus: false,
- show: true,
- kind: WindowKind::PopUp,
- is_movable: false,
- screen: Some(screen),
- }
-}
+// let screen_bounds = screen.content_bounds();
+// WindowOptions {
+// bounds: WindowBounds::Fixed(RectF::new(
+// screen_bounds.upper_right()
+// + vec2f(
+// -NOTIFICATION_PADDING - window_size.x(),
+// NOTIFICATION_PADDING,
+// ),
+// window_size,
+// )),
+// titlebar: None,
+// center: false,
+// focus: false,
+// show: true,
+// kind: WindowKind::PopUp,
+// is_movable: false,
+// screen: Some(screen),
+// }
+// }
-fn render_avatar<T: 'static>(
- avatar: Option<Arc<ImageData>>,
- avatar_style: &AvatarStyle,
- container: ContainerStyle,
-) -> AnyElement<T> {
- avatar
- .map(|avatar| {
- Image::from_data(avatar)
- .with_style(avatar_style.image)
- .aligned()
- .contained()
- .with_corner_radius(avatar_style.outer_corner_radius)
- .constrained()
- .with_width(avatar_style.outer_width)
- .with_height(avatar_style.outer_width)
- .into_any()
- })
- .unwrap_or_else(|| {
- Empty::new()
- .constrained()
- .with_width(avatar_style.outer_width)
- .into_any()
- })
- .contained()
- .with_style(container)
- .into_any()
-}
+// fn render_avatar<T: 'static>(
+// avatar: Option<Arc<ImageData>>,
+// avatar_style: &AvatarStyle,
+// container: ContainerStyle,
+// ) -> AnyElement<T> {
+// avatar
+// .map(|avatar| {
+// Image::from_data(avatar)
+// .with_style(avatar_style.image)
+// .aligned()
+// .contained()
+// .with_corner_radius(avatar_style.outer_corner_radius)
+// .constrained()
+// .with_width(avatar_style.outer_width)
+// .with_height(avatar_style.outer_width)
+// .into_any()
+// })
+// .unwrap_or_else(|| {
+// Empty::new()
+// .constrained()
+// .with_width(avatar_style.outer_width)
+// .into_any()
+// })
+// .contained()
+// .with_style(container)
+// .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>()
+// }
@@ -1,113 +1,113 @@
-use std::ops::Range;
+// use std::ops::Range;
-use gpui::{
- geometry::{
- rect::RectF,
- vector::{vec2f, Vector2F},
- },
- json::ToJson,
- serde_json::{self, json},
- AnyElement, Axis, Element, View, ViewContext,
-};
+// use gpui::{
+// geometry::{
+// rect::RectF,
+// vector::{vec2f, Vector2F},
+// },
+// json::ToJson,
+// serde_json::{self, json},
+// AnyElement, Axis, Element, View, ViewContext,
+// };
-pub(crate) struct FacePile<V: View> {
- overlap: f32,
- faces: Vec<AnyElement<V>>,
-}
+// pub(crate) struct FacePile<V: View> {
+// overlap: f32,
+// faces: Vec<AnyElement<V>>,
+// }
-impl<V: View> FacePile<V> {
- pub fn new(overlap: f32) -> Self {
- Self {
- overlap,
- faces: Vec::new(),
- }
- }
-}
+// impl<V: View> FacePile<V> {
+// pub fn new(overlap: f32) -> Self {
+// Self {
+// overlap,
+// faces: Vec::new(),
+// }
+// }
+// }
-impl<V: View> Element<V> for FacePile<V> {
- type LayoutState = ();
- type PaintState = ();
+// impl<V: View> Element<V> for FacePile<V> {
+// type LayoutState = ();
+// type PaintState = ();
- fn layout(
- &mut self,
- constraint: gpui::SizeConstraint,
- view: &mut V,
- cx: &mut ViewContext<V>,
- ) -> (Vector2F, Self::LayoutState) {
- debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
+// fn layout(
+// &mut self,
+// constraint: gpui::SizeConstraint,
+// view: &mut V,
+// cx: &mut ViewContext<V>,
+// ) -> (Vector2F, Self::LayoutState) {
+// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
- let mut width = 0.;
- let mut max_height = 0.;
- for face in &mut self.faces {
- let layout = face.layout(constraint, view, cx);
- width += layout.x();
- max_height = f32::max(max_height, layout.y());
- }
- width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
+// let mut width = 0.;
+// let mut max_height = 0.;
+// for face in &mut self.faces {
+// let layout = face.layout(constraint, view, cx);
+// width += layout.x();
+// max_height = f32::max(max_height, layout.y());
+// }
+// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
- (
- Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
- (),
- )
- }
+// (
+// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
+// (),
+// )
+// }
- fn paint(
- &mut self,
- bounds: RectF,
- visible_bounds: RectF,
- _layout: &mut Self::LayoutState,
- view: &mut V,
- cx: &mut ViewContext<V>,
- ) -> Self::PaintState {
- let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+// fn paint(
+// &mut self,
+// bounds: RectF,
+// visible_bounds: RectF,
+// _layout: &mut Self::LayoutState,
+// view: &mut V,
+// cx: &mut ViewContext<V>,
+// ) -> Self::PaintState {
+// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
- let origin_y = bounds.upper_right().y();
- let mut origin_x = bounds.upper_right().x();
+// let origin_y = bounds.upper_right().y();
+// let mut origin_x = bounds.upper_right().x();
- for face in self.faces.iter_mut().rev() {
- let size = face.size();
- origin_x -= size.x();
- let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
+// for face in self.faces.iter_mut().rev() {
+// let size = face.size();
+// origin_x -= size.x();
+// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
- cx.scene().push_layer(None);
- face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
- cx.scene().pop_layer();
- origin_x += self.overlap;
- }
+// cx.scene().push_layer(None);
+// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
+// cx.scene().pop_layer();
+// origin_x += self.overlap;
+// }
- ()
- }
+// ()
+// }
- fn rect_for_text_range(
- &self,
- _: Range<usize>,
- _: RectF,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &V,
- _: &ViewContext<V>,
- ) -> Option<RectF> {
- None
- }
+// fn rect_for_text_range(
+// &self,
+// _: Range<usize>,
+// _: RectF,
+// _: RectF,
+// _: &Self::LayoutState,
+// _: &Self::PaintState,
+// _: &V,
+// _: &ViewContext<V>,
+// ) -> Option<RectF> {
+// None
+// }
- fn debug(
- &self,
- bounds: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- _: &V,
- _: &ViewContext<V>,
- ) -> serde_json::Value {
- json!({
- "type": "FacePile",
- "bounds": bounds.to_json()
- })
- }
-}
+// fn debug(
+// &self,
+// bounds: RectF,
+// _: &Self::LayoutState,
+// _: &Self::PaintState,
+// _: &V,
+// _: &ViewContext<V>,
+// ) -> serde_json::Value {
+// json!({
+// "type": "FacePile",
+// "bounds": bounds.to_json()
+// })
+// }
+// }
-impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
- fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
- self.faces.extend(children);
- }
-}
+// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
+// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
+// self.faces.extend(children);
+// }
+// }
@@ -1,884 +1,884 @@
-use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
-use anyhow::Result;
-use channel::ChannelStore;
-use client::{Client, Notification, User, UserStore};
-use collections::HashMap;
-use db::kvp::KEY_VALUE_STORE;
-use futures::StreamExt;
-use gpui::{
- actions,
- elements::*,
- platform::{CursorStyle, MouseButton},
- serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
- ViewContext, ViewHandle, WeakViewHandle, WindowContext,
-};
-use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
-use project::Fs;
-use rpc::proto;
-use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{sync::Arc, time::Duration};
-use theme::{ui, Theme};
-use time::{OffsetDateTime, UtcOffset};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
- dock::{DockPosition, Panel},
- Workspace,
-};
-
-const LOADING_THRESHOLD: usize = 30;
-const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
-const TOAST_DURATION: Duration = Duration::from_secs(5);
-const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
-
-pub struct NotificationPanel {
- client: Arc<Client>,
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
- notification_store: ModelHandle<NotificationStore>,
- fs: Arc<dyn Fs>,
- width: Option<f32>,
- active: bool,
- notification_list: ListState<Self>,
- pending_serialization: Task<Option<()>>,
- subscriptions: Vec<gpui::Subscription>,
- workspace: WeakViewHandle<Workspace>,
- current_notification_toast: Option<(u64, Task<()>)>,
- local_timezone: UtcOffset,
- has_focus: bool,
- mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedNotificationPanel {
- width: Option<f32>,
-}
-
-#[derive(Debug)]
-pub enum Event {
- DockPositionChanged,
- Focus,
- Dismissed,
-}
-
-pub struct NotificationPresenter {
- pub actor: Option<Arc<client::User>>,
- pub text: String,
- pub icon: &'static str,
- pub needs_response: bool,
- pub can_navigate: bool,
-}
-
-actions!(notification_panel, [ToggleFocus]);
-
-pub fn init(_cx: &mut AppContext) {}
-
-impl NotificationPanel {
- pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
- let fs = workspace.app_state().fs.clone();
- let client = workspace.app_state().client.clone();
- let user_store = workspace.app_state().user_store.clone();
- let workspace_handle = workspace.weak_handle();
-
- cx.add_view(|cx| {
- let mut status = client.status();
- cx.spawn(|this, mut cx| async move {
- while let Some(_) = status.next().await {
- if this
- .update(&mut cx, |_, cx| {
- cx.notify();
- })
- .is_err()
- {
- break;
- }
- }
- })
- .detach();
-
- let mut notification_list =
- ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
- this.render_notification(ix, cx)
- .unwrap_or_else(|| Empty::new().into_any())
- });
- notification_list.set_scroll_handler(|visible_range, count, this, cx| {
- if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
- if let Some(task) = this
- .notification_store
- .update(cx, |store, cx| store.load_more_notifications(false, cx))
- {
- task.detach();
- }
- }
- });
-
- let mut this = Self {
- fs,
- client,
- user_store,
- local_timezone: cx.platform().local_timezone(),
- channel_store: ChannelStore::global(cx),
- notification_store: NotificationStore::global(cx),
- notification_list,
- pending_serialization: Task::ready(None),
- workspace: workspace_handle,
- has_focus: false,
- current_notification_toast: None,
- subscriptions: Vec::new(),
- active: false,
- mark_as_read_tasks: HashMap::default(),
- width: None,
- };
-
- let mut old_dock_position = this.position(cx);
- this.subscriptions.extend([
- cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
- cx.subscribe(&this.notification_store, Self::on_notification_event),
- 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
- })
- }
-
- pub fn load(
- workspace: WeakViewHandle<Workspace>,
- cx: AsyncAppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
- cx.spawn(|mut cx| async move {
- let serialized_panel = if let Some(panel) = cx
- .background()
- .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
- .await
- .log_err()
- .flatten()
- {
- Some(serde_json::from_str::<SerializedNotificationPanel>(&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(
- NOTIFICATION_PANEL_KEY.into(),
- serde_json::to_string(&SerializedNotificationPanel { width })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
- fn render_notification(
- &mut self,
- ix: usize,
- cx: &mut ViewContext<Self>,
- ) -> Option<AnyElement<Self>> {
- let entry = self.notification_store.read(cx).notification_at(ix)?;
- let notification_id = entry.id;
- let now = OffsetDateTime::now_utc();
- let timestamp = entry.timestamp;
- let NotificationPresenter {
- actor,
- text,
- needs_response,
- can_navigate,
- ..
- } = self.present_notification(entry, cx)?;
-
- let theme = theme::current(cx);
- let style = &theme.notification_panel;
- let response = entry.response;
- let notification = entry.notification.clone();
-
- let message_style = if entry.is_read {
- style.read_text.clone()
- } else {
- style.unread_text.clone()
- };
-
- if self.active && !entry.is_read {
- self.did_render_notification(notification_id, ¬ification, cx);
- }
-
- enum Decline {}
- enum Accept {}
-
- Some(
- MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
- let container = message_style.container;
-
- Flex::row()
- .with_children(actor.map(|actor| {
- render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
- }))
- .with_child(
- Flex::column()
- .with_child(Text::new(text, message_style.text.clone()))
- .with_child(
- Flex::row()
- .with_child(
- Label::new(
- format_timestamp(timestamp, now, self.local_timezone),
- style.timestamp.text.clone(),
- )
- .contained()
- .with_style(style.timestamp.container),
- )
- .with_children(if let Some(is_accepted) = response {
- Some(
- Label::new(
- if is_accepted {
- "You accepted"
- } else {
- "You declined"
- },
- style.read_text.text.clone(),
- )
- .flex_float()
- .into_any(),
- )
- } else if needs_response {
- Some(
- Flex::row()
- .with_children([
- MouseEventHandler::new::<Decline, _>(
- ix,
- cx,
- |state, _| {
- let button =
- style.button.style_for(state);
- Label::new(
- "Decline",
- button.text.clone(),
- )
- .contained()
- .with_style(button.container)
- },
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, {
- let notification = notification.clone();
- move |_, view, cx| {
- view.respond_to_notification(
- notification.clone(),
- false,
- cx,
- );
- }
- }),
- MouseEventHandler::new::<Accept, _>(
- ix,
- cx,
- |state, _| {
- let button =
- style.button.style_for(state);
- Label::new(
- "Accept",
- button.text.clone(),
- )
- .contained()
- .with_style(button.container)
- },
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, {
- let notification = notification.clone();
- move |_, view, cx| {
- view.respond_to_notification(
- notification.clone(),
- true,
- cx,
- );
- }
- }),
- ])
- .flex_float()
- .into_any(),
- )
- } else {
- None
- }),
- )
- .flex(1.0, true),
- )
- .contained()
- .with_style(container)
- .into_any()
- })
- .with_cursor_style(if can_navigate {
- CursorStyle::PointingHand
- } else {
- CursorStyle::default()
- })
- .on_click(MouseButton::Left, {
- let notification = notification.clone();
- move |_, this, cx| this.did_click_notification(¬ification, cx)
- })
- .into_any(),
- )
- }
-
- fn present_notification(
- &self,
- entry: &NotificationEntry,
- cx: &AppContext,
- ) -> Option<NotificationPresenter> {
- let user_store = self.user_store.read(cx);
- let channel_store = self.channel_store.read(cx);
- match entry.notification {
- Notification::ContactRequest { sender_id } => {
- let requester = user_store.get_cached_user(sender_id)?;
- Some(NotificationPresenter {
- icon: "icons/plus.svg",
- text: format!("{} wants to add you as a contact", requester.github_login),
- needs_response: user_store.has_incoming_contact_request(requester.id),
- actor: Some(requester),
- can_navigate: false,
- })
- }
- Notification::ContactRequestAccepted { responder_id } => {
- let responder = user_store.get_cached_user(responder_id)?;
- Some(NotificationPresenter {
- icon: "icons/plus.svg",
- text: format!("{} accepted your contact invite", responder.github_login),
- needs_response: false,
- actor: Some(responder),
- can_navigate: false,
- })
- }
- Notification::ChannelInvitation {
- ref channel_name,
- channel_id,
- inviter_id,
- } => {
- let inviter = user_store.get_cached_user(inviter_id)?;
- Some(NotificationPresenter {
- icon: "icons/hash.svg",
- text: format!(
- "{} invited you to join the #{channel_name} channel",
- inviter.github_login
- ),
- needs_response: channel_store.has_channel_invitation(channel_id),
- actor: Some(inviter),
- can_navigate: false,
- })
- }
- Notification::ChannelMessageMention {
- sender_id,
- channel_id,
- message_id,
- } => {
- let sender = user_store.get_cached_user(sender_id)?;
- let channel = channel_store.channel_for_id(channel_id)?;
- let message = self
- .notification_store
- .read(cx)
- .channel_message_for_id(message_id)?;
- Some(NotificationPresenter {
- icon: "icons/conversations.svg",
- text: format!(
- "{} mentioned you in #{}:\n{}",
- sender.github_login, channel.name, message.body,
- ),
- needs_response: false,
- actor: Some(sender),
- can_navigate: true,
- })
- }
- }
- }
-
- fn did_render_notification(
- &mut self,
- notification_id: u64,
- notification: &Notification,
- cx: &mut ViewContext<Self>,
- ) {
- let should_mark_as_read = match notification {
- Notification::ContactRequestAccepted { .. } => true,
- Notification::ContactRequest { .. }
- | Notification::ChannelInvitation { .. }
- | Notification::ChannelMessageMention { .. } => false,
- };
-
- if should_mark_as_read {
- self.mark_as_read_tasks
- .entry(notification_id)
- .or_insert_with(|| {
- let client = self.client.clone();
- cx.spawn(|this, mut cx| async move {
- cx.background().timer(MARK_AS_READ_DELAY).await;
- client
- .request(proto::MarkNotificationRead { notification_id })
- .await?;
- this.update(&mut cx, |this, _| {
- this.mark_as_read_tasks.remove(¬ification_id);
- })?;
- Ok(())
- })
- });
- }
- }
-
- fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
- if let Notification::ChannelMessageMention {
- message_id,
- channel_id,
- ..
- } = notification.clone()
- {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- cx.app_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
- panel.update(cx, |panel, cx| {
- panel
- .select_channel(channel_id, Some(message_id), cx)
- .detach_and_log_err(cx);
- });
- }
- });
- });
- }
- }
- }
-
- fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
- if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification {
- if let Some(workspace) = self.workspace.upgrade(cx) {
- return workspace
- .read_with(cx, |workspace, cx| {
- if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
- return panel.read_with(cx, |panel, cx| {
- panel.is_scrolled_to_bottom()
- && panel.active_chat().map_or(false, |chat| {
- chat.read(cx).channel_id == *channel_id
- })
- });
- }
- false
- })
- .unwrap_or_default();
- }
- }
-
- false
- }
-
- fn render_sign_in_prompt(
- &self,
- theme: &Arc<Theme>,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- enum SignInPromptLabel {}
-
- MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
- Label::new(
- "Sign in to view your notifications".to_string(),
- theme
- .chat_panel
- .sign_in_prompt
- .style_for(mouse_state)
- .clone(),
- )
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- let client = this.client.clone();
- cx.spawn(|_, cx| async move {
- client.authenticate_and_connect(true, &cx).log_err().await;
- })
- .detach();
- })
- .aligned()
- .into_any()
- }
-
- fn render_empty_state(
- &self,
- theme: &Arc<Theme>,
- _cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- Label::new(
- "You have no notifications".to_string(),
- theme.chat_panel.sign_in_prompt.default.clone(),
- )
- .aligned()
- .into_any()
- }
-
- fn on_notification_event(
- &mut self,
- _: ModelHandle<NotificationStore>,
- event: &NotificationEvent,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
- NotificationEvent::NotificationRemoved { entry }
- | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
- NotificationEvent::NotificationsUpdated {
- old_range,
- new_count,
- } => {
- self.notification_list.splice(old_range.clone(), *new_count);
- cx.notify();
- }
- }
- }
-
- fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
- if self.is_showing_notification(&entry.notification, cx) {
- return;
- }
-
- let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
- else {
- return;
- };
-
- let notification_id = entry.id;
- self.current_notification_toast = Some((
- notification_id,
- cx.spawn(|this, mut cx| async move {
- cx.background().timer(TOAST_DURATION).await;
- this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
- .ok();
- }),
- ));
-
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.dismiss_notification::<NotificationToast>(0, cx);
- workspace.show_notification(0, cx, |cx| {
- let workspace = cx.weak_handle();
- cx.add_view(|_| NotificationToast {
- notification_id,
- actor,
- text,
- workspace,
- })
- })
- })
- .ok();
- }
-
- fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
- if let Some((current_id, _)) = &self.current_notification_toast {
- if *current_id == notification_id {
- self.current_notification_toast.take();
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.dismiss_notification::<NotificationToast>(0, cx)
- })
- .ok();
- }
- }
- }
-
- fn respond_to_notification(
- &mut self,
- notification: Notification,
- response: bool,
- cx: &mut ViewContext<Self>,
- ) {
- self.notification_store.update(cx, |store, cx| {
- store.respond_to_notification(notification, response, cx);
- });
- }
-}
-
-impl Entity for NotificationPanel {
- type Event = Event;
-}
-
-impl View for NotificationPanel {
- fn ui_name() -> &'static str {
- "NotificationPanel"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = theme::current(cx);
- let style = &theme.notification_panel;
- let element = if self.client.user_id().is_none() {
- self.render_sign_in_prompt(&theme, cx)
- } else if self.notification_list.item_count() == 0 {
- self.render_empty_state(&theme, cx)
- } else {
- Flex::column()
- .with_child(
- Flex::row()
- .with_child(Label::new("Notifications", style.title.text.clone()))
- .with_child(ui::svg(&style.title_icon).flex_float())
- .align_children_center()
- .contained()
- .with_style(style.title.container)
- .constrained()
- .with_height(style.title_height),
- )
- .with_child(
- List::new(self.notification_list.clone())
- .contained()
- .with_style(style.list)
- .flex(1., true),
- )
- .into_any()
- };
- element
- .contained()
- .with_style(style.container)
- .constrained()
- .with_min_width(150.)
- .into_any()
- }
-
- fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = true;
- }
-
- fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
- }
-}
-
-impl Panel for NotificationPanel {
- fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
- settings::get::<NotificationPanelSettings>(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::<NotificationPanelSettings>(
- self.fs.clone(),
- cx,
- move |settings| settings.dock = Some(position),
- );
- }
-
- fn size(&self, cx: &gpui::WindowContext) -> f32 {
- self.width
- .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(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 self.notification_store.read(cx).notification_count() == 0 {
- cx.emit(Event::Dismissed);
- }
- }
-
- fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
- (settings::get::<NotificationPanelSettings>(cx).button
- && self.notification_store.read(cx).notification_count() > 0)
- .then(|| "icons/bell.svg")
- }
-
- fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
- (
- "Notification Panel".to_string(),
- Some(Box::new(ToggleFocus)),
- )
- }
-
- fn icon_label(&self, cx: &WindowContext) -> Option<String> {
- let count = self.notification_store.read(cx).unread_notification_count();
- if count == 0 {
- None
- } else {
- Some(count.to_string())
- }
- }
-
- fn should_change_position_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::DockPositionChanged)
- }
-
- fn should_close_on_event(event: &Self::Event) -> bool {
- matches!(event, Event::Dismissed)
- }
-
- fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
- self.has_focus
- }
-
- fn is_focus_event(event: &Self::Event) -> bool {
- matches!(event, Event::Focus)
- }
-}
-
-pub struct NotificationToast {
- notification_id: u64,
- actor: Option<Arc<User>>,
- text: String,
- workspace: WeakViewHandle<Workspace>,
-}
-
-pub enum ToastEvent {
- Dismiss,
-}
-
-impl NotificationToast {
- fn focus_notification_panel(&self, cx: &mut AppContext) {
- let workspace = self.workspace.clone();
- let notification_id = self.notification_id;
- cx.defer(move |cx| {
- workspace
- .update(cx, |workspace, cx| {
- if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
- panel.update(cx, |panel, cx| {
- let store = panel.notification_store.read(cx);
- if let Some(entry) = store.notification_for_id(notification_id) {
- panel.did_click_notification(&entry.clone().notification, cx);
- }
- });
- }
- })
- .ok();
- })
- }
-}
-
-impl Entity for NotificationToast {
- type Event = ToastEvent;
-}
-
-impl View for NotificationToast {
- fn ui_name() -> &'static str {
- "ContactNotification"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let user = self.actor.clone();
- let theme = theme::current(cx).clone();
- let theme = &theme.contact_notification;
-
- MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
- Flex::row()
- .with_children(user.and_then(|user| {
- Some(
- Image::from_data(user.avatar.clone()?)
- .with_style(theme.header_avatar)
- .aligned()
- .constrained()
- .with_height(
- cx.font_cache()
- .line_height(theme.header_message.text.font_size),
- )
- .aligned()
- .top(),
- )
- }))
- .with_child(
- Text::new(self.text.clone(), theme.header_message.text.clone())
- .contained()
- .with_style(theme.header_message.container)
- .aligned()
- .top()
- .left()
- .flex(1., true),
- )
- .with_child(
- MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
- let style = theme.dismiss_button.style_for(state);
- Svg::new("icons/x.svg")
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .contained()
- .with_style(style.container)
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .with_padding(Padding::uniform(5.))
- .on_click(MouseButton::Left, move |_, _, cx| {
- cx.emit(ToastEvent::Dismiss)
- })
- .aligned()
- .constrained()
- .with_height(
- cx.font_cache()
- .line_height(theme.header_message.text.font_size),
- )
- .aligned()
- .top()
- .flex_float(),
- )
- .contained()
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.focus_notification_panel(cx);
- cx.emit(ToastEvent::Dismiss);
- })
- .into_any()
- }
-}
-
-impl workspace::notifications::Notification for NotificationToast {
- fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
- matches!(event, ToastEvent::Dismiss)
- }
-}
-
-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();
- if date == today {
- let difference = now - timestamp;
- if difference >= Duration::from_secs(3600) {
- format!("{}h", difference.whole_seconds() / 3600)
- } else if difference >= Duration::from_secs(60) {
- format!("{}m", difference.whole_seconds() / 60)
- } else {
- "just now".to_string()
- }
- } else if date.next_day() == Some(today) {
- format!("yesterday")
- } else {
- format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
- }
-}
+// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
+// use anyhow::Result;
+// use channel::ChannelStore;
+// use client::{Client, Notification, User, UserStore};
+// use collections::HashMap;
+// use db::kvp::KEY_VALUE_STORE;
+// use futures::StreamExt;
+// use gpui::{
+// actions,
+// elements::*,
+// platform::{CursorStyle, MouseButton},
+// serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
+// ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+// };
+// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
+// use project::Fs;
+// use rpc::proto;
+// use serde::{Deserialize, Serialize};
+// use settings::SettingsStore;
+// use std::{sync::Arc, time::Duration};
+// use theme::{ui, Theme};
+// use time::{OffsetDateTime, UtcOffset};
+// use util::{ResultExt, TryFutureExt};
+// use workspace::{
+// dock::{DockPosition, Panel},
+// Workspace,
+// };
+
+// const LOADING_THRESHOLD: usize = 30;
+// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
+// const TOAST_DURATION: Duration = Duration::from_secs(5);
+// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
+
+// pub struct NotificationPanel {
+// client: Arc<Client>,
+// user_store: ModelHandle<UserStore>,
+// channel_store: ModelHandle<ChannelStore>,
+// notification_store: ModelHandle<NotificationStore>,
+// fs: Arc<dyn Fs>,
+// width: Option<f32>,
+// active: bool,
+// notification_list: ListState<Self>,
+// pending_serialization: Task<Option<()>>,
+// subscriptions: Vec<gpui::Subscription>,
+// workspace: WeakViewHandle<Workspace>,
+// current_notification_toast: Option<(u64, Task<()>)>,
+// local_timezone: UtcOffset,
+// has_focus: bool,
+// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
+// }
+
+// #[derive(Serialize, Deserialize)]
+// struct SerializedNotificationPanel {
+// width: Option<f32>,
+// }
+
+// #[derive(Debug)]
+// pub enum Event {
+// DockPositionChanged,
+// Focus,
+// Dismissed,
+// }
+
+// pub struct NotificationPresenter {
+// pub actor: Option<Arc<client::User>>,
+// pub text: String,
+// pub icon: &'static str,
+// pub needs_response: bool,
+// pub can_navigate: bool,
+// }
+
+// actions!(notification_panel, [ToggleFocus]);
+
+// pub fn init(_cx: &mut AppContext) {}
+
+// impl NotificationPanel {
+// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+// let fs = workspace.app_state().fs.clone();
+// let client = workspace.app_state().client.clone();
+// let user_store = workspace.app_state().user_store.clone();
+// let workspace_handle = workspace.weak_handle();
+
+// cx.add_view(|cx| {
+// let mut status = client.status();
+// cx.spawn(|this, mut cx| async move {
+// while let Some(_) = status.next().await {
+// if this
+// .update(&mut cx, |_, cx| {
+// cx.notify();
+// })
+// .is_err()
+// {
+// break;
+// }
+// }
+// })
+// .detach();
+
+// let mut notification_list =
+// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
+// this.render_notification(ix, cx)
+// .unwrap_or_else(|| Empty::new().into_any())
+// });
+// notification_list.set_scroll_handler(|visible_range, count, this, cx| {
+// if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
+// if let Some(task) = this
+// .notification_store
+// .update(cx, |store, cx| store.load_more_notifications(false, cx))
+// {
+// task.detach();
+// }
+// }
+// });
+
+// let mut this = Self {
+// fs,
+// client,
+// user_store,
+// local_timezone: cx.platform().local_timezone(),
+// channel_store: ChannelStore::global(cx),
+// notification_store: NotificationStore::global(cx),
+// notification_list,
+// pending_serialization: Task::ready(None),
+// workspace: workspace_handle,
+// has_focus: false,
+// current_notification_toast: None,
+// subscriptions: Vec::new(),
+// active: false,
+// mark_as_read_tasks: HashMap::default(),
+// width: None,
+// };
+
+// let mut old_dock_position = this.position(cx);
+// this.subscriptions.extend([
+// cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
+// cx.subscribe(&this.notification_store, Self::on_notification_event),
+// 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
+// })
+// }
+
+// pub fn load(
+// workspace: WeakViewHandle<Workspace>,
+// cx: AsyncAppContext,
+// ) -> Task<Result<ViewHandle<Self>>> {
+// cx.spawn(|mut cx| async move {
+// let serialized_panel = if let Some(panel) = cx
+// .background()
+// .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
+// .await
+// .log_err()
+// .flatten()
+// {
+// Some(serde_json::from_str::<SerializedNotificationPanel>(&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(
+// NOTIFICATION_PANEL_KEY.into(),
+// serde_json::to_string(&SerializedNotificationPanel { width })?,
+// )
+// .await?;
+// anyhow::Ok(())
+// }
+// .log_err(),
+// );
+// }
+
+// fn render_notification(
+// &mut self,
+// ix: usize,
+// cx: &mut ViewContext<Self>,
+// ) -> Option<AnyElement<Self>> {
+// let entry = self.notification_store.read(cx).notification_at(ix)?;
+// let notification_id = entry.id;
+// let now = OffsetDateTime::now_utc();
+// let timestamp = entry.timestamp;
+// let NotificationPresenter {
+// actor,
+// text,
+// needs_response,
+// can_navigate,
+// ..
+// } = self.present_notification(entry, cx)?;
+
+// let theme = theme::current(cx);
+// let style = &theme.notification_panel;
+// let response = entry.response;
+// let notification = entry.notification.clone();
+
+// let message_style = if entry.is_read {
+// style.read_text.clone()
+// } else {
+// style.unread_text.clone()
+// };
+
+// if self.active && !entry.is_read {
+// self.did_render_notification(notification_id, ¬ification, cx);
+// }
+
+// enum Decline {}
+// enum Accept {}
+
+// Some(
+// MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
+// let container = message_style.container;
+
+// Flex::row()
+// .with_children(actor.map(|actor| {
+// render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
+// }))
+// .with_child(
+// Flex::column()
+// .with_child(Text::new(text, message_style.text.clone()))
+// .with_child(
+// Flex::row()
+// .with_child(
+// Label::new(
+// format_timestamp(timestamp, now, self.local_timezone),
+// style.timestamp.text.clone(),
+// )
+// .contained()
+// .with_style(style.timestamp.container),
+// )
+// .with_children(if let Some(is_accepted) = response {
+// Some(
+// Label::new(
+// if is_accepted {
+// "You accepted"
+// } else {
+// "You declined"
+// },
+// style.read_text.text.clone(),
+// )
+// .flex_float()
+// .into_any(),
+// )
+// } else if needs_response {
+// Some(
+// Flex::row()
+// .with_children([
+// MouseEventHandler::new::<Decline, _>(
+// ix,
+// cx,
+// |state, _| {
+// let button =
+// style.button.style_for(state);
+// Label::new(
+// "Decline",
+// button.text.clone(),
+// )
+// .contained()
+// .with_style(button.container)
+// },
+// )
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, {
+// let notification = notification.clone();
+// move |_, view, cx| {
+// view.respond_to_notification(
+// notification.clone(),
+// false,
+// cx,
+// );
+// }
+// }),
+// MouseEventHandler::new::<Accept, _>(
+// ix,
+// cx,
+// |state, _| {
+// let button =
+// style.button.style_for(state);
+// Label::new(
+// "Accept",
+// button.text.clone(),
+// )
+// .contained()
+// .with_style(button.container)
+// },
+// )
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, {
+// let notification = notification.clone();
+// move |_, view, cx| {
+// view.respond_to_notification(
+// notification.clone(),
+// true,
+// cx,
+// );
+// }
+// }),
+// ])
+// .flex_float()
+// .into_any(),
+// )
+// } else {
+// None
+// }),
+// )
+// .flex(1.0, true),
+// )
+// .contained()
+// .with_style(container)
+// .into_any()
+// })
+// .with_cursor_style(if can_navigate {
+// CursorStyle::PointingHand
+// } else {
+// CursorStyle::default()
+// })
+// .on_click(MouseButton::Left, {
+// let notification = notification.clone();
+// move |_, this, cx| this.did_click_notification(¬ification, cx)
+// })
+// .into_any(),
+// )
+// }
+
+// fn present_notification(
+// &self,
+// entry: &NotificationEntry,
+// cx: &AppContext,
+// ) -> Option<NotificationPresenter> {
+// let user_store = self.user_store.read(cx);
+// let channel_store = self.channel_store.read(cx);
+// match entry.notification {
+// Notification::ContactRequest { sender_id } => {
+// let requester = user_store.get_cached_user(sender_id)?;
+// Some(NotificationPresenter {
+// icon: "icons/plus.svg",
+// text: format!("{} wants to add you as a contact", requester.github_login),
+// needs_response: user_store.has_incoming_contact_request(requester.id),
+// actor: Some(requester),
+// can_navigate: false,
+// })
+// }
+// Notification::ContactRequestAccepted { responder_id } => {
+// let responder = user_store.get_cached_user(responder_id)?;
+// Some(NotificationPresenter {
+// icon: "icons/plus.svg",
+// text: format!("{} accepted your contact invite", responder.github_login),
+// needs_response: false,
+// actor: Some(responder),
+// can_navigate: false,
+// })
+// }
+// Notification::ChannelInvitation {
+// ref channel_name,
+// channel_id,
+// inviter_id,
+// } => {
+// let inviter = user_store.get_cached_user(inviter_id)?;
+// Some(NotificationPresenter {
+// icon: "icons/hash.svg",
+// text: format!(
+// "{} invited you to join the #{channel_name} channel",
+// inviter.github_login
+// ),
+// needs_response: channel_store.has_channel_invitation(channel_id),
+// actor: Some(inviter),
+// can_navigate: false,
+// })
+// }
+// Notification::ChannelMessageMention {
+// sender_id,
+// channel_id,
+// message_id,
+// } => {
+// let sender = user_store.get_cached_user(sender_id)?;
+// let channel = channel_store.channel_for_id(channel_id)?;
+// let message = self
+// .notification_store
+// .read(cx)
+// .channel_message_for_id(message_id)?;
+// Some(NotificationPresenter {
+// icon: "icons/conversations.svg",
+// text: format!(
+// "{} mentioned you in #{}:\n{}",
+// sender.github_login, channel.name, message.body,
+// ),
+// needs_response: false,
+// actor: Some(sender),
+// can_navigate: true,
+// })
+// }
+// }
+// }
+
+// fn did_render_notification(
+// &mut self,
+// notification_id: u64,
+// notification: &Notification,
+// cx: &mut ViewContext<Self>,
+// ) {
+// let should_mark_as_read = match notification {
+// Notification::ContactRequestAccepted { .. } => true,
+// Notification::ContactRequest { .. }
+// | Notification::ChannelInvitation { .. }
+// | Notification::ChannelMessageMention { .. } => false,
+// };
+
+// if should_mark_as_read {
+// self.mark_as_read_tasks
+// .entry(notification_id)
+// .or_insert_with(|| {
+// let client = self.client.clone();
+// cx.spawn(|this, mut cx| async move {
+// cx.background().timer(MARK_AS_READ_DELAY).await;
+// client
+// .request(proto::MarkNotificationRead { notification_id })
+// .await?;
+// this.update(&mut cx, |this, _| {
+// this.mark_as_read_tasks.remove(¬ification_id);
+// })?;
+// Ok(())
+// })
+// });
+// }
+// }
+
+// fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
+// if let Notification::ChannelMessageMention {
+// message_id,
+// channel_id,
+// ..
+// } = notification.clone()
+// {
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// cx.app_context().defer(move |cx| {
+// workspace.update(cx, |workspace, cx| {
+// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+// panel.update(cx, |panel, cx| {
+// panel
+// .select_channel(channel_id, Some(message_id), cx)
+// .detach_and_log_err(cx);
+// });
+// }
+// });
+// });
+// }
+// }
+// }
+
+// fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+// if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification {
+// if let Some(workspace) = self.workspace.upgrade(cx) {
+// return workspace
+// .read_with(cx, |workspace, cx| {
+// if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
+// return panel.read_with(cx, |panel, cx| {
+// panel.is_scrolled_to_bottom()
+// && panel.active_chat().map_or(false, |chat| {
+// chat.read(cx).channel_id == *channel_id
+// })
+// });
+// }
+// false
+// })
+// .unwrap_or_default();
+// }
+// }
+
+// false
+// }
+
+// fn render_sign_in_prompt(
+// &self,
+// theme: &Arc<Theme>,
+// cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// enum SignInPromptLabel {}
+
+// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+// Label::new(
+// "Sign in to view your notifications".to_string(),
+// theme
+// .chat_panel
+// .sign_in_prompt
+// .style_for(mouse_state)
+// .clone(),
+// )
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// let client = this.client.clone();
+// cx.spawn(|_, cx| async move {
+// client.authenticate_and_connect(true, &cx).log_err().await;
+// })
+// .detach();
+// })
+// .aligned()
+// .into_any()
+// }
+
+// fn render_empty_state(
+// &self,
+// theme: &Arc<Theme>,
+// _cx: &mut ViewContext<Self>,
+// ) -> AnyElement<Self> {
+// Label::new(
+// "You have no notifications".to_string(),
+// theme.chat_panel.sign_in_prompt.default.clone(),
+// )
+// .aligned()
+// .into_any()
+// }
+
+// fn on_notification_event(
+// &mut self,
+// _: ModelHandle<NotificationStore>,
+// event: &NotificationEvent,
+// cx: &mut ViewContext<Self>,
+// ) {
+// match event {
+// NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
+// NotificationEvent::NotificationRemoved { entry }
+// | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
+// NotificationEvent::NotificationsUpdated {
+// old_range,
+// new_count,
+// } => {
+// self.notification_list.splice(old_range.clone(), *new_count);
+// cx.notify();
+// }
+// }
+// }
+
+// fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
+// if self.is_showing_notification(&entry.notification, cx) {
+// return;
+// }
+
+// let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
+// else {
+// return;
+// };
+
+// let notification_id = entry.id;
+// self.current_notification_toast = Some((
+// notification_id,
+// cx.spawn(|this, mut cx| async move {
+// cx.background().timer(TOAST_DURATION).await;
+// this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
+// .ok();
+// }),
+// ));
+
+// self.workspace
+// .update(cx, |workspace, cx| {
+// workspace.dismiss_notification::<NotificationToast>(0, cx);
+// workspace.show_notification(0, cx, |cx| {
+// let workspace = cx.weak_handle();
+// cx.add_view(|_| NotificationToast {
+// notification_id,
+// actor,
+// text,
+// workspace,
+// })
+// })
+// })
+// .ok();
+// }
+
+// fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
+// if let Some((current_id, _)) = &self.current_notification_toast {
+// if *current_id == notification_id {
+// self.current_notification_toast.take();
+// self.workspace
+// .update(cx, |workspace, cx| {
+// workspace.dismiss_notification::<NotificationToast>(0, cx)
+// })
+// .ok();
+// }
+// }
+// }
+
+// fn respond_to_notification(
+// &mut self,
+// notification: Notification,
+// response: bool,
+// cx: &mut ViewContext<Self>,
+// ) {
+// self.notification_store.update(cx, |store, cx| {
+// store.respond_to_notification(notification, response, cx);
+// });
+// }
+// }
+
+// impl Entity for NotificationPanel {
+// type Event = Event;
+// }
+
+// impl View for NotificationPanel {
+// fn ui_name() -> &'static str {
+// "NotificationPanel"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// let theme = theme::current(cx);
+// let style = &theme.notification_panel;
+// let element = if self.client.user_id().is_none() {
+// self.render_sign_in_prompt(&theme, cx)
+// } else if self.notification_list.item_count() == 0 {
+// self.render_empty_state(&theme, cx)
+// } else {
+// Flex::column()
+// .with_child(
+// Flex::row()
+// .with_child(Label::new("Notifications", style.title.text.clone()))
+// .with_child(ui::svg(&style.title_icon).flex_float())
+// .align_children_center()
+// .contained()
+// .with_style(style.title.container)
+// .constrained()
+// .with_height(style.title_height),
+// )
+// .with_child(
+// List::new(self.notification_list.clone())
+// .contained()
+// .with_style(style.list)
+// .flex(1., true),
+// )
+// .into_any()
+// };
+// element
+// .contained()
+// .with_style(style.container)
+// .constrained()
+// .with_min_width(150.)
+// .into_any()
+// }
+
+// fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+// self.has_focus = true;
+// }
+
+// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+// self.has_focus = false;
+// }
+// }
+
+// impl Panel for NotificationPanel {
+// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+// settings::get::<NotificationPanelSettings>(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::<NotificationPanelSettings>(
+// self.fs.clone(),
+// cx,
+// move |settings| settings.dock = Some(position),
+// );
+// }
+
+// fn size(&self, cx: &gpui::WindowContext) -> f32 {
+// self.width
+// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(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 self.notification_store.read(cx).notification_count() == 0 {
+// cx.emit(Event::Dismissed);
+// }
+// }
+
+// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+// (settings::get::<NotificationPanelSettings>(cx).button
+// && self.notification_store.read(cx).notification_count() > 0)
+// .then(|| "icons/bell.svg")
+// }
+
+// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+// (
+// "Notification Panel".to_string(),
+// Some(Box::new(ToggleFocus)),
+// )
+// }
+
+// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+// let count = self.notification_store.read(cx).unread_notification_count();
+// if count == 0 {
+// None
+// } else {
+// Some(count.to_string())
+// }
+// }
+
+// fn should_change_position_on_event(event: &Self::Event) -> bool {
+// matches!(event, Event::DockPositionChanged)
+// }
+
+// fn should_close_on_event(event: &Self::Event) -> bool {
+// matches!(event, Event::Dismissed)
+// }
+
+// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+// self.has_focus
+// }
+
+// fn is_focus_event(event: &Self::Event) -> bool {
+// matches!(event, Event::Focus)
+// }
+// }
+
+// pub struct NotificationToast {
+// notification_id: u64,
+// actor: Option<Arc<User>>,
+// text: String,
+// workspace: WeakViewHandle<Workspace>,
+// }
+
+// pub enum ToastEvent {
+// Dismiss,
+// }
+
+// impl NotificationToast {
+// fn focus_notification_panel(&self, cx: &mut AppContext) {
+// let workspace = self.workspace.clone();
+// let notification_id = self.notification_id;
+// cx.defer(move |cx| {
+// workspace
+// .update(cx, |workspace, cx| {
+// if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
+// panel.update(cx, |panel, cx| {
+// let store = panel.notification_store.read(cx);
+// if let Some(entry) = store.notification_for_id(notification_id) {
+// panel.did_click_notification(&entry.clone().notification, cx);
+// }
+// });
+// }
+// })
+// .ok();
+// })
+// }
+// }
+
+// impl Entity for NotificationToast {
+// type Event = ToastEvent;
+// }
+
+// impl View for NotificationToast {
+// fn ui_name() -> &'static str {
+// "ContactNotification"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// let user = self.actor.clone();
+// let theme = theme::current(cx).clone();
+// let theme = &theme.contact_notification;
+
+// MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
+// Flex::row()
+// .with_children(user.and_then(|user| {
+// Some(
+// Image::from_data(user.avatar.clone()?)
+// .with_style(theme.header_avatar)
+// .aligned()
+// .constrained()
+// .with_height(
+// cx.font_cache()
+// .line_height(theme.header_message.text.font_size),
+// )
+// .aligned()
+// .top(),
+// )
+// }))
+// .with_child(
+// Text::new(self.text.clone(), theme.header_message.text.clone())
+// .contained()
+// .with_style(theme.header_message.container)
+// .aligned()
+// .top()
+// .left()
+// .flex(1., true),
+// )
+// .with_child(
+// MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
+// let style = theme.dismiss_button.style_for(state);
+// Svg::new("icons/x.svg")
+// .with_color(style.color)
+// .constrained()
+// .with_width(style.icon_width)
+// .aligned()
+// .contained()
+// .with_style(style.container)
+// .constrained()
+// .with_width(style.button_width)
+// .with_height(style.button_width)
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .with_padding(Padding::uniform(5.))
+// .on_click(MouseButton::Left, move |_, _, cx| {
+// cx.emit(ToastEvent::Dismiss)
+// })
+// .aligned()
+// .constrained()
+// .with_height(
+// cx.font_cache()
+// .line_height(theme.header_message.text.font_size),
+// )
+// .aligned()
+// .top()
+// .flex_float(),
+// )
+// .contained()
+// })
+// .with_cursor_style(CursorStyle::PointingHand)
+// .on_click(MouseButton::Left, move |_, this, cx| {
+// this.focus_notification_panel(cx);
+// cx.emit(ToastEvent::Dismiss);
+// })
+// .into_any()
+// }
+// }
+
+// impl workspace::notifications::Notification for NotificationToast {
+// fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
+// matches!(event, ToastEvent::Dismiss)
+// }
+// }
+
+// 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();
+// if date == today {
+// let difference = now - timestamp;
+// if difference >= Duration::from_secs(3600) {
+// format!("{}h", difference.whole_seconds() / 3600)
+// } else if difference >= Duration::from_secs(60) {
+// format!("{}m", difference.whole_seconds() / 60)
+// } else {
+// "just now".to_string()
+// }
+// } else if date.next_day() == Some(today) {
+// format!("yesterday")
+// } else {
+// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+// }
+// }
@@ -1,11 +1,11 @@
-use gpui::AppContext;
-use std::sync::Arc;
-use workspace::AppState;
+// use gpui::AppContext;
+// use std::sync::Arc;
+// use workspace::AppState;
-pub mod incoming_call_notification;
-pub mod project_shared_notification;
+// pub mod incoming_call_notification;
+// pub mod project_shared_notification;
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
- incoming_call_notification::init(app_state, cx);
- project_shared_notification::init(app_state, cx);
-}
+// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+// incoming_call_notification::init(app_state, cx);
+// project_shared_notification::init(app_state, cx);
+// }
@@ -1,7 +1,7 @@
use anyhow;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
@@ -32,37 +32,37 @@ pub struct PanelSettingsContent {
pub default_width: Option<f32>,
}
-impl Setting for CollaborationPanelSettings {
+impl Settings for CollaborationPanelSettings {
const KEY: Option<&'static str> = Some("collaboration_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
-impl Setting for ChatPanelSettings {
+impl Settings for ChatPanelSettings {
const KEY: Option<&'static str> = Some("chat_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
-impl Setting for NotificationPanelSettings {
+impl Settings for NotificationPanelSettings {
const KEY: Option<&'static str> = Some("notification_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
- _: &gpui::AppContext,
+ _: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
@@ -36,11 +36,10 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
- actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
- AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
- EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render,
- Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext,
- WindowHandle, WindowOptions,
+ actions, div, point, prelude::*, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
+ AsyncAppContext, AsyncWindowContext, Bounds, Div, Entity, EntityId, EventEmitter, GlobalPixels,
+ KeyContext, Model, ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription,
+ Task, View, ViewContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -68,8 +67,6 @@ use std::{
};
use theme2::{ActiveTheme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use ui::TextColor;
-use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
use util::ResultExt;
use uuid::Uuid;
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
@@ -447,7 +444,7 @@ pub struct Workspace {
last_active_view_id: Option<proto::ViewId>,
status_bar: View<StatusBar>,
modal_layer: View<ModalLayer>,
- // titlebar_item: Option<AnyViewHandle>,
+ titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
follower_states: HashMap<View<Pane>, FollowerState>,
@@ -660,7 +657,7 @@ impl Workspace {
last_active_view_id: None,
status_bar,
modal_layer,
- // titlebar_item: None,
+ titlebar_item: None,
notifications: Default::default(),
left_dock,
bottom_dock,
@@ -1033,15 +1030,14 @@ impl Workspace {
&self.app_state.client
}
- // todo!()
- // pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
- // self.titlebar_item = Some(item);
- // cx.notify();
- // }
+ pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
+ self.titlebar_item = Some(item);
+ cx.notify();
+ }
- // pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
- // self.titlebar_item.clone()
- // }
+ pub fn titlebar_item(&self) -> Option<AnyView> {
+ self.titlebar_item.clone()
+ }
/// Call the given callback with a workspace whose project is local.
///
@@ -2448,75 +2444,6 @@ impl Workspace {
// .any(|state| state.leader_id == peer_id)
// }
- fn render_titlebar(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
- h_stack()
- .id("titlebar")
- .justify_between()
- .when(
- !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
- |s| s.pl_20(),
- )
- .w_full()
- .h(rems(1.75))
- .bg(cx.theme().colors().title_bar_background)
- .on_click(|_, event, cx| {
- if event.up.click_count == 2 {
- cx.zoom_window();
- }
- })
- .child(
- h_stack()
- // TODO - Add player menu
- .child(
- div()
- .id("project_owner_indicator")
- .child(
- Button::new("player")
- .variant(ButtonVariant::Ghost)
- .color(Some(TextColor::Player(0))),
- )
- .tooltip(move |_, cx| {
- cx.build_view(|cx| TextTooltip::new("Toggle following"))
- }),
- )
- // TODO - Add project menu
- .child(
- div()
- .id("titlebar_project_menu_button")
- .child(Button::new("project_name").variant(ButtonVariant::Ghost))
- .tooltip(move |_, cx| {
- cx.build_view(|cx| TextTooltip::new("Recent Projects"))
- }),
- )
- // TODO - Add git menu
- .child(
- div()
- .id("titlebar_git_menu_button")
- .child(
- Button::new("branch_name")
- .variant(ButtonVariant::Ghost)
- .color(Some(TextColor::Muted)),
- )
- .tooltip(move |_, cx| {
- // todo!() Replace with real action.
- #[gpui::action]
- struct NoAction {}
-
- cx.build_view(|cx| {
- TextTooltip::new("Recent Branches")
- .key_binding(KeyBinding::new(gpui::KeyBinding::new(
- "cmd-b",
- NoAction {},
- None,
- )))
- .meta("Only local branches shown")
- })
- }),
- ),
- ) // self.titlebar_item
- .child(h_stack().child(Label::new("Right side titlebar item")))
- }
-
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
let active_entry = self.active_project_path(cx);
self.project
@@ -3719,7 +3646,7 @@ impl Render for Workspace {
.items_start()
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().background)
- .child(self.render_titlebar(cx))
+ .children(self.titlebar_item.clone())
.child(
// todo! should this be a component a view?
div()
@@ -207,7 +207,7 @@ fn main() {
// activity_indicator::init(cx);
// language_tools::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
- // collab_ui::init(&app_state, cx);
+ collab_ui::init(&app_state, cx);
// feedback::init(cx);
// welcome::init(cx);
// zed::init(&app_state, cx);
@@ -341,10 +341,6 @@ pub fn initialize_workspace(
// workspace.active_pane().clone(),
// ));
- // let collab_titlebar_item =
- // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
- // workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
-
// let copilot =
// cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
// let diagnostic_summary =