From 793fa6e3a4ded5c095ab0414e7fa682f657050db Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 15 Nov 2023 15:47:33 -0700 Subject: [PATCH] Move placeholder titlebar render to collab_ui --- Cargo.lock | 1 + Cargo.toml | 1 + crates/collab_ui2/Cargo.toml | 1 + crates/collab_ui2/src/channel_view.rs | 804 +- crates/collab_ui2/src/chat_panel.rs | 1966 ++--- crates/collab_ui2/src/collab_panel.rs | 7096 ++++++++--------- crates/collab_ui2/src/collab_titlebar_item.rs | 2405 +++--- 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, 7323 insertions(+), 7316 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de80a9a7b3..e2cf75f34a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1864,6 +1864,7 @@ dependencies = [ "theme2", "time", "tree-sitter-markdown", + "ui2", "util", "workspace2", "zed_actions2", diff --git a/Cargo.toml b/Cargo.toml index 6b29b18127..fdf37405a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/collab", "crates/collab2", "crates/collab_ui", + "crates/collab_ui2", "crates/collections", "crates/command_palette", "crates/command_palette2", diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index 8c48e09846..4660880ecd 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -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"} diff --git a/crates/collab_ui2/src/channel_view.rs b/crates/collab_ui2/src/channel_view.rs index fe46f3bb3e..d2ffc0de57 100644 --- a/crates/collab_ui2/src/channel_view.rs +++ b/crates/collab_ui2/src/channel_view.rs @@ -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, -}; +// 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]); +// actions!(channel_view, [Deploy]); -pub fn init(cx: &mut AppContext) { - register_followable_item::(cx) -} +// pub fn init(cx: &mut AppContext) { +// register_followable_item::(cx) +// } -pub struct ChannelView { - pub editor: ViewHandle, - project: ModelHandle, - channel_store: ModelHandle, - channel_buffer: ModelHandle, - remote_id: Option, - _editor_event_subscription: Subscription, -} +// pub struct ChannelView { +// pub editor: ViewHandle, +// project: ModelHandle, +// channel_store: ModelHandle, +// channel_buffer: ModelHandle, +// remote_id: Option, +// _editor_event_subscription: Subscription, +// } -impl ChannelView { - pub fn open( - channel_id: ChannelId, - workspace: ViewHandle, - cx: &mut AppContext, - ) -> Task>> { - 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) - }) - } +// impl ChannelView { +// pub fn open( +// channel_id: ChannelId, +// workspace: ViewHandle, +// cx: &mut AppContext, +// ) -> Task>> { +// 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, - workspace: ViewHandle, - cx: &mut AppContext, - ) -> Task>> { - 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)); +// pub fn open_in_pane( +// channel_id: ChannelId, +// pane: ViewHandle, +// workspace: ViewHandle, +// cx: &mut AppContext, +// ) -> Task>> { +// 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(); +// 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); - } - }) - }); +// 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); +// pane.update(&mut cx, |pane, cx| { +// let buffer_id = channel_buffer.read(cx).remote_id(cx); - let existing_view = pane - .items_of_type::() - .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); +// let existing_view = pane +// .items_of_type::() +// .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; - } - } +// // 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 - }); +// 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); - } - } +// // 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")) - }) - } +// view +// }) +// .ok_or_else(|| anyhow!("pane was dropped")) +// }) +// } - pub fn new( - project: ModelHandle, - channel_store: ModelHandle, - channel_buffer: ModelHandle, - cx: &mut ViewContext, - ) -> 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())); +// pub fn new( +// project: ModelHandle, +// channel_store: ModelHandle, +// channel_buffer: ModelHandle, +// cx: &mut ViewContext, +// ) -> 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(); +// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) +// .detach(); - Self { - editor, - project, - channel_store, - channel_buffer, - remote_id: None, - _editor_event_subscription, - } - } +// Self { +// editor, +// project, +// channel_store, +// channel_buffer, +// remote_id: None, +// _editor_event_subscription, +// } +// } - pub fn channel(&self, cx: &AppContext) -> Option> { - self.channel_buffer.read(cx).channel(cx) - } +// pub fn channel(&self, cx: &AppContext) -> Option> { +// self.channel_buffer.read(cx).channel(cx) +// } - fn handle_channel_buffer_event( - &mut self, - _: ModelHandle, - event: &ChannelBufferEvent, - cx: &mut ViewContext, - ) { - 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 handle_channel_buffer_event( +// &mut self, +// _: ModelHandle, +// event: &ChannelBufferEvent, +// cx: &mut ViewContext, +// ) { +// 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); - }); - } -} +// 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 Entity for ChannelView { +// type Event = editor::Event; +// } -impl View for ChannelView { - fn ui_name() -> &'static str { - "ChannelView" - } +// impl View for ChannelView { +// fn ui_name() -> &'static str { +// "ChannelView" +// } - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(self.editor.as_any(), cx).into_any() - } +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// ChildView::new(self.editor.as_any(), cx).into_any() +// } - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - self.acknowledge_buffer_version(cx); - cx.focus(self.editor.as_any()) - } - } -} +// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { +// 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, - _: &'a AppContext, - ) -> Option<&'a AnyViewHandle> { - if type_id == TypeId::of::() { - Some(self_handle) - } else if type_id == TypeId::of::() { - Some(&self.editor) - } else { - None - } - } +// impl Item for ChannelView { +// fn act_as_type<'a>( +// &'a self, +// type_id: TypeId, +// self_handle: &'a ViewHandle, +// _: &'a AppContext, +// ) -> Option<&'a AnyViewHandle> { +// if type_id == TypeId::of::() { +// Some(self_handle) +// } else if type_id == TypeId::of::() { +// Some(&self.editor) +// } else { +// None +// } +// } - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - cx: &gpui::AppContext, - ) -> AnyElement { - 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 tab_content( +// &self, +// _: Option, +// style: &theme::Tab, +// cx: &gpui::AppContext, +// ) -> AnyElement { +// 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) -> Option { - Some(Self::new( - self.project.clone(), - self.channel_store.clone(), - self.channel_buffer.clone(), - cx, - )) - } +// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { +// Some(Self::new( +// self.project.clone(), +// self.channel_store.clone(), +// self.channel_buffer.clone(), +// cx, +// )) +// } - fn is_singleton(&self, _cx: &AppContext) -> bool { - false - } +// fn is_singleton(&self, _cx: &AppContext) -> bool { +// false +// } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { - self.editor - .update(cx, |editor, cx| editor.navigate(data, cx)) - } +// fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { +// self.editor +// .update(cx, |editor, cx| editor.navigate(data, cx)) +// } - fn deactivated(&mut self, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| Item::deactivated(editor, cx)) - } +// fn deactivated(&mut self, cx: &mut ViewContext) { +// self.editor +// .update(cx, |editor, cx| Item::deactivated(editor, cx)) +// } - fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) - } +// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { +// self.editor +// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) +// } - fn as_searchable(&self, _: &ViewHandle) -> Option> { - Some(Box::new(self.editor.clone())) - } +// fn as_searchable(&self, _: &ViewHandle) -> Option> { +// Some(Box::new(self.editor.clone())) +// } - fn show_toolbar(&self) -> bool { - true - } +// fn show_toolbar(&self) -> bool { +// true +// } - fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { - self.editor.read(cx).pixel_position_of_cursor(cx) - } +// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { +// 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) - } -} +// 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 { - self.remote_id - } +// impl FollowableItem for ChannelView { +// fn remote_id(&self) -> Option { +// self.remote_id +// } - fn to_state_proto(&self, cx: &AppContext) -> Option { - let channel_buffer = self.channel_buffer.read(cx); - if !channel_buffer.is_connected() { - return None; - } +// fn to_state_proto(&self, cx: &AppContext) -> Option { +// 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 - }, - }, - )) - } +// 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: ViewHandle, - remote_id: workspace::ViewId, - state: &mut Option, - cx: &mut AppContext, - ) -> Option>>> { - let Some(proto::view::Variant::ChannelView(_)) = state else { - return None; - }; - let Some(proto::view::Variant::ChannelView(state)) = state.take() else { - unreachable!() - }; +// fn from_state_proto( +// pane: ViewHandle, +// workspace: ViewHandle, +// remote_id: workspace::ViewId, +// state: &mut Option, +// cx: &mut AppContext, +// ) -> Option>>> { +// 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); +// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); - Some(cx.spawn(|mut cx| async move { - let this = open.await?; +// 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); +// 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(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?; - } +// if let Some(task) = task { +// task.await?; +// } - Ok(this) - })) - } +// Ok(this) +// })) +// } - fn add_event_to_update_proto( - &self, - event: &Self::Event, - update: &mut Option, - cx: &AppContext, - ) -> bool { - self.editor - .read(cx) - .add_event_to_update_proto(event, update, cx) - } +// fn add_event_to_update_proto( +// &self, +// event: &Self::Event, +// update: &mut Option, +// cx: &AppContext, +// ) -> bool { +// self.editor +// .read(cx) +// .add_event_to_update_proto(event, update, cx) +// } - fn apply_update_proto( - &mut self, - project: &ModelHandle, - message: proto::update_view::Variant, - cx: &mut ViewContext, - ) -> gpui::Task> { - self.editor.update(cx, |editor, cx| { - editor.apply_update_proto(project, message, cx) - }) - } +// fn apply_update_proto( +// &mut self, +// project: &ModelHandle, +// message: proto::update_view::Variant, +// cx: &mut ViewContext, +// ) -> gpui::Task> { +// self.editor.update(cx, |editor, cx| { +// editor.apply_update_proto(project, message, cx) +// }) +// } - fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - editor.set_leader_peer_id(leader_peer_id, cx) - }) - } +// fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { +// 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 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 - } -} +// fn is_project_item(&self, _cx: &AppContext) -> bool { +// false +// } +// } -struct ChannelBufferCollaborationHub(ModelHandle); +// struct ChannelBufferCollaborationHub(ModelHandle); -impl CollaborationHub for ChannelBufferCollaborationHub { - fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { - self.0.read(cx).collaborators() - } +// impl CollaborationHub for ChannelBufferCollaborationHub { +// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { +// self.0.read(cx).collaborators() +// } - fn user_participant_indices<'a>( - &self, - cx: &'a AppContext, - ) -> &'a HashMap { - self.0.read(cx).user_store().read(cx).participant_indices() - } -} +// fn user_participant_indices<'a>( +// &self, +// cx: &'a AppContext, +// ) -> &'a HashMap { +// self.0.read(cx).user_store().read(cx).participant_indices() +// } +// } diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs index 5a4dafb6d4..e9e2610a86 100644 --- a/crates/collab_ui2/src/chat_panel.rs +++ b/crates/collab_ui2/src/chat_panel.rs @@ -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, - channel_store: ModelHandle, - languages: Arc, - active_chat: Option<(ModelHandle, Subscription)>, - message_list: ListState, - input_editor: ViewHandle, - channel_select: ViewHandle, - ) -> AnyElement, +// local_timezone: UtcOffset, +// fs: Arc, +// width: Option, +// active: bool, +// pending_serialization: Task>, +// subscriptions: Vec, +// workspace: WeakViewHandle, +// is_scrolled_to_bottom: bool, +// has_focus: bool, +// markdown_data: HashMap, +// } + +// #[derive(Serialize, Deserialize)] +// struct SerializedChatPanel { +// width: Option, +// } + +// #[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) -> ViewHandle { +// 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::::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::(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> { +// self.active_chat.as_ref().map(|(chat, _)| chat.clone()) +// } + +// pub fn load( +// workspace: WeakViewHandle, +// cx: AsyncAppContext, +// ) -> Task>> { +// 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::(&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) { +// 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) { +// 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, cx: &mut ViewContext) { +// 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, +// event: &ChannelChatEvent, +// cx: &mut ViewContext, +// ) { +// 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) -> AnyElement { +// 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) -> AnyElement { +// 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) -> AnyElement { +// 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::(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, +// 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::>(); + +// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) +// } + +// fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { +// ChildView::new(&self.input_editor, cx) +// .contained() +// .with_style(theme.chat_panel.input_editor.container) +// .into_any() +// } + +// fn render_channel_name( +// channel_store: &ModelHandle, +// ix: usize, +// item_type: ItemType, +// is_hovered: bool, +// workspace: WeakViewHandle, +// cx: &mut ViewContext { +// 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::(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::( +// channel_id as usize, +// "Open Notes", +// Some(Box::new(OpenChannelNotes)), +// tooltip_style.clone(), +// cx, +// ) +// .flex_float(), +// MouseEventHandler::new::(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::( +// 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, +// cx: &mut ViewContext, +// ) -> AnyElement { +// enum SignInPromptLabel {} + +// MouseEventHandler::new::(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) { +// 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) { +// 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) { +// 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, +// cx: &mut ViewContext, +// ) -> Task> { +// 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) { +// 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) { +// 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, +// cx: &mut ViewContext<'_, '_, ChatPanel>, +// theme: &Arc, +// ) -> AnyElement { +// enum DeleteMessage {} + +// message_id_to_remove +// .map(|id| { +// MouseEventHandler::new::(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) -> AnyElement { +// 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.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.has_focus = false; +// } +// } + +// impl Panel for ChatPanel { +// fn position(&self, cx: &gpui::WindowContext) -> DockPosition { +// settings::get::(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) { +// settings::update_settings_file::(self.fs.clone(), cx, move |settings| { +// settings.dock = Some(position) +// }); +// } + +// fn size(&self, cx: &gpui::WindowContext) -> f32 { +// self.width +// .unwrap_or_else(|| settings::get::(cx).default_width) +// } + +// fn set_size(&mut self, size: Option, cx: &mut ViewContext) { +// self.width = size; +// self.serialize(cx); +// cx.notify(); +// } + +// fn set_active(&mut self, active: bool, cx: &mut ViewContext) { +// 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::(cx).button && is_channels_feature_enabled(cx)) +// .then(|| "icons/conversations.svg") +// } + +// fn icon_tooltip(&self) -> (String, Option>) { +// ("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(style: &IconButton, svg_path: &'static str) -> impl Element { +// 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 +// }, +// ] +// ); +// } +// } diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index a0b87e6476..9ba8d7f9b2 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -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::(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| { - if panel.selection.take() != Some(action.ix) { - panel.selection = Some(action.ix) - } - - cx.notify(); - }, - ); - - cx.add_action( - |panel: &mut CollabPanel, - action: &StartMoveChannelFor, - _: &mut ViewContext| { - panel.channel_clipboard = Some(ChannelMoveClipboard { - channel_id: action.channel_id, - }); - }, - ); - - cx.add_action( - |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { - 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| { - 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| { - 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, - pending_name: Option, - }, - Rename { - location: ChannelId, - pending_name: Option, - }, -} - -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, - fs: Arc, - has_focus: bool, - channel_clipboard: Option, - pending_serialization: Task>, - context_menu: ViewHandle, - filter_editor: ViewHandle, - channel_name_editor: ViewHandle, - channel_editing_state: Option, - entries: Vec, - selection: Option, - user_store: ModelHandle, - client: Arc, - channel_store: ModelHandle, - project: ModelHandle, - match_candidates: Vec, - list_state: ListState, - subscriptions: Vec, - collapsed_sections: Vec
, - collapsed_channels: Vec, - drag_target_channel: ChannelDragTarget, - workspace: WeakViewHandle, - context_menu_on_selected: bool, -} - -#[derive(PartialEq, Eq)] -enum ChannelDragTarget { - None, - Root, - Channel(ChannelId), -} - -#[derive(Serialize, Deserialize)] -struct SerializedCollabPanel { - width: Option, - collapsed_channels: Option>, -} - -#[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, - peer_id: Option, - is_pending: bool, - }, - ParticipantProject { - project_id: u64, - worktree_root_names: Vec, - host_user_id: u64, - is_last: bool, - }, - ParticipantScreen { - peer_id: Option, - is_last: bool, - }, - IncomingRequest(Arc), - OutgoingRequest(Arc), - ChannelInvite(Arc), - Channel { - channel: Arc, - depth: usize, - has_children: bool, - }, - ChannelNotes { - channel_id: ChannelId, - }, - ChannelChat { - channel_id: ChannelId, - }, - ChannelEditor { - depth: usize, - }, - Contact { - contact: Arc, - calling: bool, - }, - ContactPlaceholder, -} - -impl Entity for CollabPanel { - type Event = Event; -} - -impl CollabPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { - cx.add_view::(|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::::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::(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::(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, - cx: AsyncAppContext, - ) -> Task>> { - 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::(&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) { - 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) { - 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::() { - 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::, _>(|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, - user_store: ModelHandle, - is_pending: bool, - is_selected: bool, - theme: &theme::Theme, - cx: &mut ViewContext, - ) -> AnyElement { - 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::( - 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::(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::( - 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::( - 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, - ) -> AnyElement { - 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::(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::( - project_id as usize, - format!("Open {}", project_name), - None, - theme.tooltip.clone(), - cx, - ) - .into_any() - } - - fn render_participant_screen( - peer_id: Option, - is_last: bool, - is_selected: bool, - theme: &theme::CollabPanel, - cx: &mut ViewContext, - ) -> AnyElement { - 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::( - 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) -> 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, - ) -> AnyElement { - 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::(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::( - 0, - channel_tooltip_text.unwrap(), - None, - tooltip_style.clone(), - cx, - ) - }), - Section::Contacts => Some( - MouseEventHandler::new::(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::( - 0, - "Search for new contact", - None, - tooltip_style.clone(), - cx, - ), - ), - Section::Channels => { - if cx - .global::>() - .currently_dragged::(cx.window()) - .is_some() - && self.drag_target_channel == ChannelDragTarget::Root - { - is_dragged_over = true; - } - - Some( - MouseEventHandler::new::(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::( - 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::(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::>() - .currently_dragged::(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::>() - .currently_dragged::(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, - theme: &theme::Theme, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - 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.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::( - 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::( - contact.user.id as usize, - label, - None, - theme.tooltip.clone(), - cx, - ) - .into_any() - } else { - event_handler - .with_tooltip::( - 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, - ) -> AnyElement { - enum AddContacts {} - MouseEventHandler::new::(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 { - 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, - ) -> AnyElement { - 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::>() - .currently_dragged::(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::(ix, cx, |state, cx| { - let row_hovered = state.hovered(); - - let mut select_state = |interactive: &Interactive| { - 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::::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::( - 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::(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::( - ix, - "Open channel chat", - None, - theme.tooltip.clone(), - cx, - ) - .contained() - .with_margin_right(4.), - ) - .with_child( - MouseEventHandler::new::(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::( - 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::>() - .currently_dragged::(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::>() - .currently_dragged::(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| { - let theme = &theme::current(cx).collab_panel; - - 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.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, - ) -> AnyElement { - enum ChannelNotes {} - let host_avatar_width = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - - MouseEventHandler::new::(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::::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, - ) -> AnyElement { - enum ChannelChat {} - let host_avatar_width = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - - MouseEventHandler::new::(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::::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_store: ModelHandle, - theme: &theme::CollabPanel, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - 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::(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::(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_store: ModelHandle, - theme: &theme::CollabPanel, - is_incoming: bool, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - 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::(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::(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::(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, - channel: &Channel, - ix: usize, - cx: &mut ViewContext, - ) { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) -> 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) { - 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, - ) { - 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) { - 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.toggle_channel_collapsed(action.location, cx); - } - - fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { - 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) { - 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) { - 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.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.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.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); - } - - fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { - self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); - } - - fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { - if let Some(channel) = self.selected_channel() { - self.remove_channel(channel.id, cx) - } - } - - fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { - 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) { - 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) { - 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) { - 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> { - 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, - ) { - 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.remove_channel(action.channel_id, cx) - } - - fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { - 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) { - 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.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.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>, - cx: &mut ViewContext, - ) { - 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) { - let Some(workspace) = self.workspace.upgrade(cx) else { - return; - }; - let Some(handle) = cx.window().downcast::() 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) { - 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::(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) { - 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 { - 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) { - 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.has_focus = false; - } - - fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - 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::(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::(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::(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) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings| settings.dock = Some(position), - ); - } - - fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| settings::get::(cx).default_width) - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - self.serialize(cx); - cx.notify(); - } - - fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { - settings::get::(cx) - .button - .then(|| "icons/user_group_16.svg") - } - - fn icon_tooltip(&self) -> (String, Option>) { - ( - "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 { - 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::(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| { +// if panel.selection.take() != Some(action.ix) { +// panel.selection = Some(action.ix) +// } + +// cx.notify(); +// }, +// ); + +// cx.add_action( +// |panel: &mut CollabPanel, +// action: &StartMoveChannelFor, +// _: &mut ViewContext| { +// panel.channel_clipboard = Some(ChannelMoveClipboard { +// channel_id: action.channel_id, +// }); +// }, +// ); + +// cx.add_action( +// |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { +// 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| { +// 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| { +// 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, +// pending_name: Option, +// }, +// Rename { +// location: ChannelId, +// pending_name: Option, +// }, +// } + +// 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, +// fs: Arc, +// has_focus: bool, +// channel_clipboard: Option, +// pending_serialization: Task>, +// context_menu: ViewHandle, +// filter_editor: ViewHandle, +// channel_name_editor: ViewHandle, +// channel_editing_state: Option, +// entries: Vec, +// selection: Option, +// user_store: ModelHandle, +// client: Arc, +// channel_store: ModelHandle, +// project: ModelHandle, +// match_candidates: Vec, +// list_state: ListState, +// subscriptions: Vec, +// collapsed_sections: Vec
, +// collapsed_channels: Vec, +// drag_target_channel: ChannelDragTarget, +// workspace: WeakViewHandle, +// context_menu_on_selected: bool, +// } + +// #[derive(PartialEq, Eq)] +// enum ChannelDragTarget { +// None, +// Root, +// Channel(ChannelId), +// } + +// #[derive(Serialize, Deserialize)] +// struct SerializedCollabPanel { +// width: Option, +// collapsed_channels: Option>, +// } + +// #[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, +// peer_id: Option, +// is_pending: bool, +// }, +// ParticipantProject { +// project_id: u64, +// worktree_root_names: Vec, +// host_user_id: u64, +// is_last: bool, +// }, +// ParticipantScreen { +// peer_id: Option, +// is_last: bool, +// }, +// IncomingRequest(Arc), +// OutgoingRequest(Arc), +// ChannelInvite(Arc), +// Channel { +// channel: Arc, +// depth: usize, +// has_children: bool, +// }, +// ChannelNotes { +// channel_id: ChannelId, +// }, +// ChannelChat { +// channel_id: ChannelId, +// }, +// ChannelEditor { +// depth: usize, +// }, +// Contact { +// contact: Arc, +// calling: bool, +// }, +// ContactPlaceholder, +// } + +// impl Entity for CollabPanel { +// type Event = Event; +// } + +// impl CollabPanel { +// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { +// cx.add_view::(|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::::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::(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::(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, +// cx: AsyncAppContext, +// ) -> Task>> { +// 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::(&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) { +// 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) { +// 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::() { +// 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::, _>(|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, +// user_store: ModelHandle, +// is_pending: bool, +// is_selected: bool, +// theme: &theme::Theme, +// cx: &mut ViewContext, +// ) -> AnyElement { +// 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::( +// 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::(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::( +// 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::( +// 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, +// ) -> AnyElement { +// 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::(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::( +// project_id as usize, +// format!("Open {}", project_name), +// None, +// theme.tooltip.clone(), +// cx, +// ) +// .into_any() +// } + +// fn render_participant_screen( +// peer_id: Option, +// is_last: bool, +// is_selected: bool, +// theme: &theme::CollabPanel, +// cx: &mut ViewContext, +// ) -> AnyElement { +// 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::( +// 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) -> 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, +// ) -> AnyElement { +// 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::(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::( +// 0, +// channel_tooltip_text.unwrap(), +// None, +// tooltip_style.clone(), +// cx, +// ) +// }), +// Section::Contacts => Some( +// MouseEventHandler::new::(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::( +// 0, +// "Search for new contact", +// None, +// tooltip_style.clone(), +// cx, +// ), +// ), +// Section::Channels => { +// if cx +// .global::>() +// .currently_dragged::(cx.window()) +// .is_some() +// && self.drag_target_channel == ChannelDragTarget::Root +// { +// is_dragged_over = true; +// } + +// Some( +// MouseEventHandler::new::(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::( +// 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::(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::>() +// .currently_dragged::(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::>() +// .currently_dragged::(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, +// theme: &theme::Theme, +// is_selected: bool, +// cx: &mut ViewContext, +// ) -> AnyElement { +// 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.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::( +// 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::( +// contact.user.id as usize, +// label, +// None, +// theme.tooltip.clone(), +// cx, +// ) +// .into_any() +// } else { +// event_handler +// .with_tooltip::( +// 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, +// ) -> AnyElement { +// enum AddContacts {} +// MouseEventHandler::new::(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 { +// 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, +// ) -> AnyElement { +// 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::>() +// .currently_dragged::(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::(ix, cx, |state, cx| { +// let row_hovered = state.hovered(); + +// let mut select_state = |interactive: &Interactive| { +// 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::::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::( +// 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::(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::( +// ix, +// "Open channel chat", +// None, +// theme.tooltip.clone(), +// cx, +// ) +// .contained() +// .with_margin_right(4.), +// ) +// .with_child( +// MouseEventHandler::new::(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::( +// 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::>() +// .currently_dragged::(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::>() +// .currently_dragged::(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| { +// let theme = &theme::current(cx).collab_panel; + +// 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.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, +// ) -> AnyElement { +// enum ChannelNotes {} +// let host_avatar_width = theme +// .contact_avatar +// .width +// .or(theme.contact_avatar.height) +// .unwrap_or(0.); + +// MouseEventHandler::new::(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::::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, +// ) -> AnyElement { +// enum ChannelChat {} +// let host_avatar_width = theme +// .contact_avatar +// .width +// .or(theme.contact_avatar.height) +// .unwrap_or(0.); + +// MouseEventHandler::new::(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::::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_store: ModelHandle, +// theme: &theme::CollabPanel, +// is_selected: bool, +// cx: &mut ViewContext, +// ) -> AnyElement { +// 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::(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::(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_store: ModelHandle, +// theme: &theme::CollabPanel, +// is_incoming: bool, +// is_selected: bool, +// cx: &mut ViewContext, +// ) -> AnyElement { +// 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::(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::(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::(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, +// channel: &Channel, +// ix: usize, +// cx: &mut ViewContext, +// ) { +// 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) { +// 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) { +// 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) { +// 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) { +// 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) { +// 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) -> 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) { +// 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, +// ) { +// 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) { +// 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.toggle_channel_collapsed(action.location, cx); +// } + +// fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { +// 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) { +// 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) { +// 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.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.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.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); +// } + +// fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { +// self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); +// } + +// fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { +// if let Some(channel) = self.selected_channel() { +// self.remove_channel(channel.id, cx) +// } +// } + +// fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { +// 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) { +// 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) { +// 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) { +// 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> { +// 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, +// ) { +// 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.remove_channel(action.channel_id, cx) +// } + +// fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { +// 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) { +// 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.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.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>, +// cx: &mut ViewContext, +// ) { +// 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) { +// let Some(workspace) = self.workspace.upgrade(cx) else { +// return; +// }; +// let Some(handle) = cx.window().downcast::() 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) { +// 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::(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) { +// 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 { +// 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) { +// 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.has_focus = false; +// } + +// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { +// 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::(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::(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::(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) { +// settings::update_settings_file::( +// self.fs.clone(), +// cx, +// move |settings| settings.dock = Some(position), +// ); +// } + +// fn size(&self, cx: &gpui::WindowContext) -> f32 { +// self.width +// .unwrap_or_else(|| settings::get::(cx).default_width) +// } + +// fn set_size(&mut self, size: Option, cx: &mut ViewContext) { +// self.width = size; +// self.serialize(cx); +// cx.notify(); +// } + +// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { +// settings::get::(cx) +// .button +// .then(|| "icons/user_group_16.svg") +// } + +// fn icon_tooltip(&self) -> (String, Option>) { +// ( +// "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 { +// 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) +// } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index cef8faf601..c1b7928209 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -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}; +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; +// const MAX_PROJECT_NAME_LENGTH: usize = 40; +// const MAX_BRANCH_NAME_LENGTH: usize = 40; -actions!( - collab, - [ - ToggleUserMenu, - ToggleProjectMenu, - SwitchBranch, - ShareProject, - UnshareProject, - ] -); +// 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, - user_store: ModelHandle, + project: Model, + #[allow(unused)] // todo!() + user_store: Model, + #[allow(unused)] // todo!() client: Arc, - workspace: WeakViewHandle, - branch_popover: Option>, - project_popover: Option>, - user_menu: ViewHandle, + #[allow(unused)] // todo!() + workspace: WeakView, + //branch_popover: Option>, + //project_popover: Option>, + //user_menu: ViewHandle, _subscriptions: Vec, } -impl Entity for CollabTitlebarItem { - type Event = (); -} +impl Render for CollabTitlebarItem { + type Element = Stateful>; -impl View for CollabTitlebarItem { - fn ui_name() -> &'static str { - "CollabTitlebarItem" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - 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(), + fn render(&mut self, cx: &mut ViewContext) -> 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) -> AnyElement { +// 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, - cx: &mut ViewContext, - ) -> Self { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> 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, - cx: &mut ViewContext, - ) -> AnyElement { - let project = self.project.read(cx); + // fn collect_title_root_names( + // &self, + // theme: Arc, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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()) - }); + // 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)) - }; + // 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 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(); + // let mut ret = Flex::row(); - if let Some(project_host) = self.collect_project_host(theme.clone(), cx) { - ret = ret.with_child(project_host) - } + // 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::(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::( - 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::(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::( - 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() - } + // ret = ret.with_child( + // Stack::new() + // .with_child( + // MouseEventHandler::new::(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::( + // 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::(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::( + // 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, - cx: &mut ViewContext, - ) -> Option> { - 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); + // fn collect_project_host( + // &self, + // theme: Arc, + // cx: &mut ViewContext, + // ) -> Option> { + // 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; - } + // 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; - }; + // 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 {} + // 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(); + // 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::(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::( - 0, - host_user.github_login.clone() + " is sharing this project. Click to follow.", - None, - theme.tooltip.clone(), - cx, - ) - .into_any_named("project-host"), - ) - } + // Some( + // MouseEventHandler::new::(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::( + // 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) { - let project = if active { + fn window_activation_changed(&mut self, cx: &mut ViewContext) { + 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) { - 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 share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { + // 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) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.unshare_project(project, cx)) - .log_err(); - } + // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext) { + // 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.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); - }); - } + // pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { + // 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, - ) -> Option> { - self.branch_popover.as_ref().map(|child| { - let theme = theme::current(cx).clone(); - let child = ChildView::new(child, cx); - let child = MouseEventHandler::new::(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(); + // fn render_branches_popover_host<'a>( + // &'a self, + // _theme: &'a theme::Titlebar, + // cx: &'a mut ViewContext, + // ) -> Option> { + // self.branch_popover.as_ref().map(|child| { + // let theme = theme::current(cx).clone(); + // let child = ChildView::new(child, cx); + // let child = MouseEventHandler::new::(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() - }) - } + // 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, - ) -> Option> { - self.project_popover.as_ref().map(|child| { - let theme = theme::current(cx).clone(); - let child = ChildView::new(child, cx); - let child = MouseEventHandler::new::(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(); + // fn render_project_popover_host<'a>( + // &'a self, + // _theme: &'a theme::Titlebar, + // cx: &'a mut ViewContext, + // ) -> Option> { + // self.project_popover.as_ref().map(|child| { + // let theme = theme::current(cx).clone(); + // let child = ChildView::new(child, cx); + // let child = MouseEventHandler::new::(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() - }) - } + // 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) { - 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; - } - } + // pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext) { + // 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(); + // }) + // .detach(); + // self.project_popover.take(); + // cx.focus(&view); + // self.branch_popover = Some(view); + // } + // } - cx.notify(); - } + // cx.notify(); + // } - pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { - 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(); + // pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext) { + // 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)); + // 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.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(); - } + // 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, - cx: &mut ViewContext, - ) -> AnyElement { - 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"; - } + // fn render_toggle_screen_sharing_button( + // &self, + // theme: &Theme, + // room: &ModelHandle, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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::(0, cx, |state, _| { - let style = titlebar - .screen_share_button - .in_state(active) - .style_for(state); + // let active = room.read(cx).is_screen_sharing(); + // let titlebar = &theme.titlebar; + // MouseEventHandler::new::(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::( - 0, - tooltip, - Some(Box::new(ToggleScreenSharing)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any() - } - fn render_toggle_mute( - &self, - theme: &Theme, - room: &ModelHandle, - cx: &mut ViewContext, - ) -> AnyElement { - 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"; - } + // 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::( + // 0, + // tooltip, + // Some(Box::new(ToggleScreenSharing)), + // theme.tooltip.clone(), + // cx, + // ) + // .aligned() + // .into_any() + // } + // fn render_toggle_mute( + // &self, + // theme: &Theme, + // room: &ModelHandle, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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::(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::( - 0, - tooltip, - Some(Box::new(ToggleMute)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any() - } - fn render_toggle_deafen( - &self, - theme: &Theme, - room: &ModelHandle, - cx: &mut ViewContext, - ) -> AnyElement { - 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::(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::( + // 0, + // tooltip, + // Some(Box::new(ToggleMute)), + // theme.tooltip.clone(), + // cx, + // ) + // .aligned() + // .into_any() + // } + // fn render_toggle_deafen( + // &self, + // theme: &Theme, + // room: &ModelHandle, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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::(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::( - 0, - tooltip, - Some(Box::new(ToggleDeafen)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any() - } - fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - let icon = "icons/exit.svg"; - let tooltip = "Leave call"; + // let titlebar = &theme.titlebar; + // MouseEventHandler::new::(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::( + // 0, + // tooltip, + // Some(Box::new(ToggleDeafen)), + // theme.tooltip.clone(), + // cx, + // ) + // .aligned() + // .into_any() + // } + // fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { + // let icon = "icons/exit.svg"; + // let tooltip = "Leave call"; - let titlebar = &theme.titlebar; - MouseEventHandler::new::(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::( - 0, - tooltip, - Some(Box::new(LeaveCall)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any() - } - fn render_in_call_share_unshare_button( - &self, - workspace: &ViewHandle, - theme: &Theme, - cx: &mut ViewContext, - ) -> Option> { - let project = workspace.read(cx).project(); - if project.read(cx).is_remote() { - return None; - } + // let titlebar = &theme.titlebar; + // MouseEventHandler::new::(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::( + // 0, + // tooltip, + // Some(Box::new(LeaveCall)), + // theme.tooltip.clone(), + // cx, + // ) + // .aligned() + // .into_any() + // } + // fn render_in_call_share_unshare_button( + // &self, + // workspace: &ViewHandle, + // theme: &Theme, + // cx: &mut ViewContext, + // ) -> Option> { + // 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 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; + // let titlebar = &theme.titlebar; - enum ShareUnshare {} - Some( - Stack::new() - .with_child( - MouseEventHandler::new::(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::( - 0, - tooltip.to_owned(), - None, - theme.tooltip.clone(), - cx, - ), - ) - .aligned() - .contained() - .with_margin_left(theme.titlebar.item_spacing) - .into_any(), - ) - } + // enum ShareUnshare {} + // Some( + // Stack::new() + // .with_child( + // MouseEventHandler::new::(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::( + // 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>, - cx: &mut ViewContext, - ) -> AnyElement { - 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 - }; + // fn render_user_menu_button( + // &self, + // theme: &Theme, + // avatar: Option>, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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::(0, cx, |state, _| { - let style = user_menu_button_style - .user_menu - .inactive_state() - .style_for(state); + // let avatar_style = &user_menu_button_style.avatar; + // Stack::new() + // .with_child( + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = user_menu_button_style + // .user_menu + // .inactive_state() + // .style_for(state); - let mut dropdown = Flex::row().align_children_center(); + // 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, - )); - }; + // 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::( - 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() - } + // 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::( + // 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) -> AnyElement { - let titlebar = &theme.titlebar; - MouseEventHandler::new::(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_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { + // let titlebar = &theme.titlebar; + // MouseEventHandler::new::(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, - theme: &Theme, - room: &ModelHandle, - cx: &mut ViewContext, - ) -> Vec> { - let mut participants = room - .read(cx) - .remote_participants() - .values() - .cloned() - .collect::>(); - participants.sort_by_cached_key(|p| p.user.github_login.clone()); + // fn render_collaborators( + // &self, + // workspace: &ViewHandle, + // theme: &Theme, + // room: &ModelHandle, + // cx: &mut ViewContext, + // ) -> Vec> { + // let mut participants = room + // .read(cx) + // .remote_participants() + // .values() + // .cloned() + // .collect::>(); + // 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() - } + // 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, - theme: &Theme, - user: &Arc, - peer_id: PeerId, - muted: bool, - speaking: bool, - cx: &mut ViewContext, - ) -> AnyElement { - let replica_id = workspace.read(cx).project().read(cx).replica_id(); + // fn render_current_user( + // &self, + // workspace: &ViewHandle, + // theme: &Theme, + // user: &Arc, + // peer_id: PeerId, + // muted: bool, + // speaking: bool, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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() - } + // 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, - peer_id: PeerId, - location: Option, - muted: bool, - speaking: bool, - workspace: &ViewHandle, - theme: &Theme, - cx: &mut ViewContext, - ) -> AnyElement { - 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), - }); + // fn render_face_pile( + // &self, + // user: &User, + // _replica_id: Option, + // peer_id: PeerId, + // location: Option, + // muted: bool, + // speaking: bool, + // workspace: &ViewHandle, + // theme: &Theme, + // cx: &mut ViewContext, + // ) -> AnyElement { + // 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 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 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 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; - } - } + // 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 {} + // enum TitlebarParticipant {} - let content = MouseEventHandler::new::( - 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 content = MouseEventHandler::new::( + // 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); + // 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); - } - } + // 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(), - ) - })()) - }, - ); + // 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(); - } + // 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::( - peer_id.as_u64() as usize, - format!("Follow {}", user.github_login), - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .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::( + // 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, - location: Option, - mut style: AvatarStyle, - cx: &ViewContext, - ) -> 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; - } - } + // fn location_style( + // workspace: &ViewHandle, + // location: Option, + // mut style: AvatarStyle, + // cx: &ViewContext, + // ) -> 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 - } + // style + // } - fn render_face( - avatar: Arc, - avatar_style: AvatarStyle, - background_color: Color, - microphone_state: Option, - ) -> AnyElement { - 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_face( + // avatar: Arc, + // avatar_style: AvatarStyle, + // background_color: Color, + // microphone_state: Option, + // ) -> AnyElement { + // 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, - ) -> Option> { - enum ConnectionStatusButton {} + // fn render_connection_status( + // &self, + // status: &client::Status, + // cx: &mut ViewContext, + // ) -> Option> { + // 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" - } - }; + // 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::(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, - } - } + // Some( + // MouseEventHandler::new::(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, -} +// pub struct AvatarRibbon { +// color: Color, +// } -impl AvatarRibbon { - pub fn new(color: Color) -> AvatarRibbon { - AvatarRibbon { color } - } -} +// impl AvatarRibbon { +// pub fn new(color: Color) -> AvatarRibbon { +// AvatarRibbon { color } +// } +// } -impl Element for AvatarRibbon { - type LayoutState = (); +// impl Element for AvatarRibbon { +// type LayoutState = (); - type PaintState = (); +// type PaintState = (); - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - _: &mut CollabTitlebarItem, - _: &mut ViewContext, - ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { - (constraint.max, ()) - } +// fn layout( +// &mut self, +// constraint: gpui::SizeConstraint, +// _: &mut CollabTitlebarItem, +// _: &mut ViewContext, +// ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { +// (constraint.max, ()) +// } - fn paint( - &mut self, - bounds: RectF, - _: RectF, - _: &mut Self::LayoutState, - _: &mut CollabTitlebarItem, - cx: &mut ViewContext, - ) -> 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 paint( +// &mut self, +// bounds: RectF, +// _: RectF, +// _: &mut Self::LayoutState, +// _: &mut CollabTitlebarItem, +// cx: &mut ViewContext, +// ) -> 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, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &CollabTitlebarItem, - _: &ViewContext, - ) -> Option { - None - } +// fn rect_for_text_range( +// &self, +// _: Range, +// _: RectF, +// _: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &CollabTitlebarItem, +// _: &ViewContext, +// ) -> Option { +// None +// } - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &CollabTitlebarItem, - _: &ViewContext, - ) -> gpui::json::Value { - json::json!({ - "type": "AvatarRibbon", - "bounds": bounds.to_json(), - "color": self.color.to_json(), - }) - } -} +// fn debug( +// &self, +// bounds: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &CollabTitlebarItem, +// _: &ViewContext, +// ) -> gpui::json::Value { +// json::json!({ +// "type": "AvatarRibbon", +// "bounds": bounds.to_json(), +// "color": self.color.to_json(), +// }) +// } +// } diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index fb78173536..7dc96c0e59 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -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, cx: &mut AppContext) { - settings::register::(cx); - settings::register::(cx); - settings::register::(cx); +pub fn init(_app_state: &Arc, 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, - window_size: Vector2F, -) -> WindowOptions<'static> { - const NOTIFICATION_PADDING: f32 = 16.; +// fn notification_window_options( +// screen: Rc, +// 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( - avatar: Option>, - avatar_style: &AvatarStyle, - container: ContainerStyle, -) -> AnyElement { - 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( +// avatar: Option>, +// avatar_style: &AvatarStyle, +// container: ContainerStyle, +// ) -> AnyElement { +// 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::() -} +// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool { +// cx.is_staff() || cx.has_flag::() +// } diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 5017666f7b..077b813fbd 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -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 { - overlap: f32, - faces: Vec>, -} +// pub(crate) struct FacePile { +// overlap: f32, +// faces: Vec>, +// } -impl FacePile { - pub fn new(overlap: f32) -> Self { - Self { - overlap, - faces: Vec::new(), - } - } -} +// impl FacePile { +// pub fn new(overlap: f32) -> Self { +// Self { +// overlap, +// faces: Vec::new(), +// } +// } +// } -impl Element for FacePile { - type LayoutState = (); - type PaintState = (); +// impl Element for FacePile { +// type LayoutState = (); +// type PaintState = (); - fn layout( - &mut self, - constraint: gpui::SizeConstraint, - view: &mut V, - cx: &mut ViewContext, - ) -> (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, +// ) -> (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, - ) -> 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, +// ) -> 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, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> Option { - None - } +// fn rect_for_text_range( +// &self, +// _: Range, +// _: RectF, +// _: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &V, +// _: &ViewContext, +// ) -> Option { +// None +// } - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &V, - _: &ViewContext, - ) -> serde_json::Value { - json!({ - "type": "FacePile", - "bounds": bounds.to_json() - }) - } -} +// fn debug( +// &self, +// bounds: RectF, +// _: &Self::LayoutState, +// _: &Self::PaintState, +// _: &V, +// _: &ViewContext, +// ) -> serde_json::Value { +// json!({ +// "type": "FacePile", +// "bounds": bounds.to_json() +// }) +// } +// } -impl Extend> for FacePile { - fn extend>>(&mut self, children: T) { - self.faces.extend(children); - } -} +// impl Extend> for FacePile { +// fn extend>>(&mut self, children: T) { +// self.faces.extend(children); +// } +// } diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 1720c8dcdc..4b5a99a0ed 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -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, - user_store: ModelHandle, - channel_store: ModelHandle, - notification_store: ModelHandle, - fs: Arc, - width: Option, - active: bool, - notification_list: ListState, - pending_serialization: Task>, - subscriptions: Vec, - workspace: WeakViewHandle, - current_notification_toast: Option<(u64, Task<()>)>, - local_timezone: UtcOffset, - has_focus: bool, - mark_as_read_tasks: HashMap>>, -} - -#[derive(Serialize, Deserialize)] -struct SerializedNotificationPanel { - width: Option, -} - -#[derive(Debug)] -pub enum Event { - DockPositionChanged, - Focus, - Dismissed, -} - -pub struct NotificationPresenter { - pub actor: Option>, - 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) -> ViewHandle { - 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::::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::(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, - cx: AsyncAppContext, - ) -> Task>> { - 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::(&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) { - 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, - ) -> Option> { - 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::(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::( - 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::( - 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 { - 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, - ) { - 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) { - 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::(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::(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, - cx: &mut ViewContext, - ) -> AnyElement { - enum SignInPromptLabel {} - - MouseEventHandler::new::(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, - _cx: &mut ViewContext, - ) -> AnyElement { - 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, - event: &NotificationEvent, - cx: &mut ViewContext, - ) { - 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) { - 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::(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) { - 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::(0, cx) - }) - .ok(); - } - } - } - - fn respond_to_notification( - &mut self, - notification: Notification, - response: bool, - cx: &mut ViewContext, - ) { - 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) -> AnyElement { - 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.has_focus = true; - } - - fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } -} - -impl Panel for NotificationPanel { - fn position(&self, cx: &gpui::WindowContext) -> DockPosition { - settings::get::(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) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings| settings.dock = Some(position), - ); - } - - fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| settings::get::(cx).default_width) - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - self.serialize(cx); - cx.notify(); - } - - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - 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::(cx).button - && self.notification_store.read(cx).notification_count() > 0) - .then(|| "icons/bell.svg") - } - - fn icon_tooltip(&self) -> (String, Option>) { - ( - "Notification Panel".to_string(), - Some(Box::new(ToggleFocus)), - ) - } - - fn icon_label(&self, cx: &WindowContext) -> Option { - 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>, - text: String, - workspace: WeakViewHandle, -} - -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::(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) -> AnyElement { - let user = self.actor.clone(); - let theme = theme::current(cx).clone(); - let theme = &theme.contact_notification; - - MouseEventHandler::new::(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::(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: &::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, +// user_store: ModelHandle, +// channel_store: ModelHandle, +// notification_store: ModelHandle, +// fs: Arc, +// width: Option, +// active: bool, +// notification_list: ListState, +// pending_serialization: Task>, +// subscriptions: Vec, +// workspace: WeakViewHandle, +// current_notification_toast: Option<(u64, Task<()>)>, +// local_timezone: UtcOffset, +// has_focus: bool, +// mark_as_read_tasks: HashMap>>, +// } + +// #[derive(Serialize, Deserialize)] +// struct SerializedNotificationPanel { +// width: Option, +// } + +// #[derive(Debug)] +// pub enum Event { +// DockPositionChanged, +// Focus, +// Dismissed, +// } + +// pub struct NotificationPresenter { +// pub actor: Option>, +// 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) -> ViewHandle { +// 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::::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::(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, +// cx: AsyncAppContext, +// ) -> Task>> { +// 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::(&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) { +// 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, +// ) -> Option> { +// 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::(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::( +// 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::( +// 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 { +// 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, +// ) { +// 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) { +// 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::(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::(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, +// cx: &mut ViewContext, +// ) -> AnyElement { +// enum SignInPromptLabel {} + +// MouseEventHandler::new::(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, +// _cx: &mut ViewContext, +// ) -> AnyElement { +// 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, +// event: &NotificationEvent, +// cx: &mut ViewContext, +// ) { +// 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) { +// 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::(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) { +// 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::(0, cx) +// }) +// .ok(); +// } +// } +// } + +// fn respond_to_notification( +// &mut self, +// notification: Notification, +// response: bool, +// cx: &mut ViewContext, +// ) { +// 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) -> AnyElement { +// 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.has_focus = true; +// } + +// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { +// self.has_focus = false; +// } +// } + +// impl Panel for NotificationPanel { +// fn position(&self, cx: &gpui::WindowContext) -> DockPosition { +// settings::get::(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) { +// settings::update_settings_file::( +// self.fs.clone(), +// cx, +// move |settings| settings.dock = Some(position), +// ); +// } + +// fn size(&self, cx: &gpui::WindowContext) -> f32 { +// self.width +// .unwrap_or_else(|| settings::get::(cx).default_width) +// } + +// fn set_size(&mut self, size: Option, cx: &mut ViewContext) { +// self.width = size; +// self.serialize(cx); +// cx.notify(); +// } + +// fn set_active(&mut self, active: bool, cx: &mut ViewContext) { +// 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::(cx).button +// && self.notification_store.read(cx).notification_count() > 0) +// .then(|| "icons/bell.svg") +// } + +// fn icon_tooltip(&self) -> (String, Option>) { +// ( +// "Notification Panel".to_string(), +// Some(Box::new(ToggleFocus)), +// ) +// } + +// fn icon_label(&self, cx: &WindowContext) -> Option { +// 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>, +// text: String, +// workspace: WeakViewHandle, +// } + +// 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::(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) -> AnyElement { +// let user = self.actor.clone(); +// let theme = theme::current(cx).clone(); +// let theme = &theme.contact_notification; + +// MouseEventHandler::new::(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::(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: &::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()) +// } +// } diff --git a/crates/collab_ui2/src/notifications.rs b/crates/collab_ui2/src/notifications.rs index 5c184ec5c8..bc5d7ad3bf 100644 --- a/crates/collab_ui2/src/notifications.rs +++ b/crates/collab_ui2/src/notifications.rs @@ -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, cx: &mut AppContext) { - incoming_call_notification::init(app_state, cx); - project_shared_notification::init(app_state, cx); -} +// pub fn init(app_state: &Arc, cx: &mut AppContext) { +// incoming_call_notification::init(app_state, cx); +// project_shared_notification::init(app_state, cx); +// } diff --git a/crates/collab_ui2/src/panel_settings.rs b/crates/collab_ui2/src/panel_settings.rs index f8678d774e..3d062951a6 100644 --- a/crates/collab_ui2/src/panel_settings.rs +++ b/crates/collab_ui2/src/panel_settings.rs @@ -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, } -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::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::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::load_via_json_merge(default_value, user_values) } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 6c2d0c0ede..5213a1f83d 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -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, status_bar: View, modal_layer: View, - // titlebar_item: Option, + titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, follower_states: HashMap, 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.titlebar_item = Some(item); - // cx.notify(); - // } + pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext) { + self.titlebar_item = Some(item); + cx.notify(); + } - // pub fn titlebar_item(&self) -> Option { - // self.titlebar_item.clone() - // } + pub fn titlebar_item(&self) -> Option { + 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) -> impl Component { - 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) { 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() diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2a3d4d1195..d24245bd1c 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -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); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 2f7a38b041..8e9079ed6b 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -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 =