collab ui2 (#3339)

- Move placeholder titlebar render to collab_ui

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin
2023-11-15 16:21:52 -07:00
committed by GitHub
21 changed files with 9447 additions and 93 deletions
Generated
+42
View File
@@ -1829,6 +1829,47 @@ dependencies = [
"zed-actions",
]
[[package]]
name = "collab_ui2"
version = "0.1.0"
dependencies = [
"anyhow",
"call2",
"channel2",
"client2",
"clock",
"collections",
"db2",
"editor2",
"feature_flags2",
"futures 0.3.28",
"fuzzy",
"gpui2",
"language2",
"lazy_static",
"log",
"menu2",
"notifications2",
"picker2",
"postage",
"pretty_assertions",
"project2",
"rich_text2",
"rpc2",
"schemars",
"serde",
"serde_derive",
"settings2",
"smallvec",
"theme2",
"time",
"tree-sitter-markdown",
"ui2",
"util",
"workspace2",
"zed_actions2",
]
[[package]]
name = "collections"
version = "0.1.0"
@@ -11441,6 +11482,7 @@ dependencies = [
"chrono",
"cli",
"client2",
"collab_ui2",
"collections",
"command_palette2",
"copilot2",
+1
View File
@@ -18,6 +18,7 @@ members = [
"crates/collab",
"crates/collab2",
"crates/collab_ui",
"crates/collab_ui2",
"crates/collections",
"crates/command_palette",
"crates/command_palette2",
+81
View File
@@ -0,0 +1,81 @@
[package]
name = "collab_ui2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/collab_ui.rs"
doctest = false
[features]
test-support = [
"call/test-support",
"client/test-support",
"collections/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"settings/test-support",
"util/test-support",
"workspace/test-support",
]
[dependencies]
# auto_update = { path = "../auto_update" }
db = { package = "db2", path = "../db2" }
call = { package = "call2", path = "../call2" }
client = { package = "client2", path = "../client2" }
channel = { package = "channel2", path = "../channel2" }
clock = { path = "../clock" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
# drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" }
#feedback = { path = "../feedback" }
fuzzy = { path = "../fuzzy" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
notifications = { package = "notifications2", path = "../notifications2" }
rich_text = { package = "rich_text2", path = "../rich_text2" }
picker = { package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
# recent_projects = { path = "../recent_projects" }
rpc = { package ="rpc2", path = "../rpc2" }
settings = { package = "settings2", path = "../settings2" }
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"}
anyhow.workspace = true
futures.workspace = true
lazy_static.workspace = true
log.workspace = true
schemars.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
time.workspace = true
smallvec.workspace = true
[dev-dependencies]
call = { package = "call2", path = "../call2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
project = { package = "project2", path = "../project2", features = ["test-support"] }
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
pretty_assertions.workspace = true
tree-sitter-markdown.workspace = true
+454
View File
@@ -0,0 +1,454 @@
// use anyhow::{anyhow, Result};
// use call::report_call_event_for_channel;
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
// use client::{
// proto::{self, PeerId},
// Collaborator, ParticipantIndex,
// };
// use collections::HashMap;
// use editor::{CollaborationHub, Editor};
// use gpui::{
// actions,
// elements::{ChildView, Label},
// geometry::vector::Vector2F,
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
// ViewContext, ViewHandle,
// };
// use project::Project;
// use smallvec::SmallVec;
// use std::{
// any::{Any, TypeId},
// sync::Arc,
// };
// use util::ResultExt;
// use workspace::{
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
// register_followable_item,
// searchable::SearchableItemHandle,
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
// };
// actions!(channel_view, [Deploy]);
// pub fn init(cx: &mut AppContext) {
// register_followable_item::<ChannelView>(cx)
// }
// pub struct ChannelView {
// pub editor: ViewHandle<Editor>,
// project: ModelHandle<Project>,
// channel_store: ModelHandle<ChannelStore>,
// channel_buffer: ModelHandle<ChannelBuffer>,
// remote_id: Option<ViewId>,
// _editor_event_subscription: Subscription,
// }
// impl ChannelView {
// pub fn open(
// channel_id: ChannelId,
// workspace: ViewHandle<Workspace>,
// cx: &mut AppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// let pane = workspace.read(cx).active_pane().clone();
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
// cx.spawn(|mut cx| async move {
// let channel_view = channel_view.await?;
// pane.update(&mut cx, |pane, cx| {
// report_call_event_for_channel(
// "open channel notes",
// channel_id,
// &workspace.read(cx).app_state().client,
// cx,
// );
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
// });
// anyhow::Ok(channel_view)
// })
// }
// pub fn open_in_pane(
// channel_id: ChannelId,
// pane: ViewHandle<Pane>,
// workspace: ViewHandle<Workspace>,
// cx: &mut AppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// let workspace = workspace.read(cx);
// let project = workspace.project().to_owned();
// let channel_store = ChannelStore::global(cx);
// let language_registry = workspace.app_state().languages.clone();
// let markdown = language_registry.language_for_name("Markdown");
// let channel_buffer =
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
// cx.spawn(|mut cx| async move {
// let channel_buffer = channel_buffer.await?;
// let markdown = markdown.await.log_err();
// channel_buffer.update(&mut cx, |buffer, cx| {
// buffer.buffer().update(cx, |buffer, cx| {
// buffer.set_language_registry(language_registry);
// if let Some(markdown) = markdown {
// buffer.set_language(Some(markdown), cx);
// }
// })
// });
// pane.update(&mut cx, |pane, cx| {
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
// let existing_view = pane
// .items_of_type::<Self>()
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// // If this channel buffer is already open in this pane, just return it.
// if let Some(existing_view) = existing_view.clone() {
// if existing_view.read(cx).channel_buffer == channel_buffer {
// return existing_view;
// }
// }
// let view = cx.add_view(|cx| {
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
// this.acknowledge_buffer_version(cx);
// this
// });
// // If the pane contained a disconnected view for this channel buffer,
// // replace that.
// if let Some(existing_item) = existing_view {
// if let Some(ix) = pane.index_for_item(&existing_item) {
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
// .detach();
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
// }
// }
// view
// })
// .ok_or_else(|| anyhow!("pane was dropped"))
// })
// }
// pub fn new(
// project: ModelHandle<Project>,
// channel_store: ModelHandle<ChannelStore>,
// channel_buffer: ModelHandle<ChannelBuffer>,
// cx: &mut ViewContext<Self>,
// ) -> Self {
// let buffer = channel_buffer.read(cx).buffer();
// let editor = cx.add_view(|cx| {
// let mut editor = Editor::for_buffer(buffer, None, cx);
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
// channel_buffer.clone(),
// )));
// editor.set_read_only(
// !channel_buffer
// .read(cx)
// .channel(cx)
// .is_some_and(|c| c.can_edit_notes()),
// );
// editor
// });
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
// .detach();
// Self {
// editor,
// project,
// channel_store,
// channel_buffer,
// remote_id: None,
// _editor_event_subscription,
// }
// }
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
// self.channel_buffer.read(cx).channel(cx)
// }
// fn handle_channel_buffer_event(
// &mut self,
// _: ModelHandle<ChannelBuffer>,
// event: &ChannelBufferEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
// editor.set_read_only(true);
// cx.notify();
// }),
// ChannelBufferEvent::ChannelChanged => {
// self.editor.update(cx, |editor, cx| {
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
// cx.emit(editor::Event::TitleChanged);
// cx.notify()
// });
// }
// ChannelBufferEvent::BufferEdited => {
// if cx.is_self_focused() || self.editor.is_focused(cx) {
// self.acknowledge_buffer_version(cx);
// } else {
// self.channel_store.update(cx, |store, cx| {
// let channel_buffer = self.channel_buffer.read(cx);
// store.notes_changed(
// channel_buffer.channel_id,
// channel_buffer.epoch(),
// &channel_buffer.buffer().read(cx).version(),
// cx,
// )
// });
// }
// }
// ChannelBufferEvent::CollaboratorsChanged => {}
// }
// }
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
// self.channel_store.update(cx, |store, cx| {
// let channel_buffer = self.channel_buffer.read(cx);
// store.acknowledge_notes_version(
// channel_buffer.channel_id,
// channel_buffer.epoch(),
// &channel_buffer.buffer().read(cx).version(),
// cx,
// )
// });
// self.channel_buffer.update(cx, |buffer, cx| {
// buffer.acknowledge_buffer_version(cx);
// });
// }
// }
// impl Entity for ChannelView {
// type Event = editor::Event;
// }
// impl View for ChannelView {
// fn ui_name() -> &'static str {
// "ChannelView"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// ChildView::new(self.editor.as_any(), cx).into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// if cx.is_self_focused() {
// self.acknowledge_buffer_version(cx);
// cx.focus(self.editor.as_any())
// }
// }
// }
// impl Item for ChannelView {
// fn act_as_type<'a>(
// &'a self,
// type_id: TypeId,
// self_handle: &'a ViewHandle<Self>,
// _: &'a AppContext,
// ) -> Option<&'a AnyViewHandle> {
// if type_id == TypeId::of::<Self>() {
// Some(self_handle)
// } else if type_id == TypeId::of::<Editor>() {
// Some(&self.editor)
// } else {
// None
// }
// }
// fn tab_content<V: 'static>(
// &self,
// _: Option<usize>,
// style: &theme::Tab,
// cx: &gpui::AppContext,
// ) -> AnyElement<V> {
// let label = if let Some(channel) = self.channel(cx) {
// match (
// channel.can_edit_notes(),
// self.channel_buffer.read(cx).is_connected(),
// ) {
// (true, true) => format!("#{}", channel.name),
// (false, true) => format!("#{} (read-only)", channel.name),
// (_, false) => format!("#{} (disconnected)", channel.name),
// }
// } else {
// format!("channel notes (disconnected)")
// };
// Label::new(label, style.label.to_owned()).into_any()
// }
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
// Some(Self::new(
// self.project.clone(),
// self.channel_store.clone(),
// self.channel_buffer.clone(),
// cx,
// ))
// }
// fn is_singleton(&self, _cx: &AppContext) -> bool {
// false
// }
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
// self.editor
// .update(cx, |editor, cx| editor.navigate(data, cx))
// }
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
// self.editor
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
// }
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
// self.editor
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
// }
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
// Some(Box::new(self.editor.clone()))
// }
// fn show_toolbar(&self) -> bool {
// true
// }
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
// self.editor.read(cx).pixel_position_of_cursor(cx)
// }
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
// editor::Editor::to_item_events(event)
// }
// }
// impl FollowableItem for ChannelView {
// fn remote_id(&self) -> Option<workspace::ViewId> {
// self.remote_id
// }
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
// let channel_buffer = self.channel_buffer.read(cx);
// if !channel_buffer.is_connected() {
// return None;
// }
// Some(proto::view::Variant::ChannelView(
// proto::view::ChannelView {
// channel_id: channel_buffer.channel_id,
// editor: if let Some(proto::view::Variant::Editor(proto)) =
// self.editor.read(cx).to_state_proto(cx)
// {
// Some(proto)
// } else {
// None
// },
// },
// ))
// }
// fn from_state_proto(
// pane: ViewHandle<workspace::Pane>,
// workspace: ViewHandle<workspace::Workspace>,
// remote_id: workspace::ViewId,
// state: &mut Option<proto::view::Variant>,
// cx: &mut AppContext,
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
// let Some(proto::view::Variant::ChannelView(_)) = state else {
// return None;
// };
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
// unreachable!()
// };
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
// Some(cx.spawn(|mut cx| async move {
// let this = open.await?;
// let task = this
// .update(&mut cx, |this, cx| {
// this.remote_id = Some(remote_id);
// if let Some(state) = state.editor {
// Some(this.editor.update(cx, |editor, cx| {
// editor.apply_update_proto(
// &this.project,
// proto::update_view::Variant::Editor(proto::update_view::Editor {
// selections: state.selections,
// pending_selection: state.pending_selection,
// scroll_top_anchor: state.scroll_top_anchor,
// scroll_x: state.scroll_x,
// scroll_y: state.scroll_y,
// ..Default::default()
// }),
// cx,
// )
// }))
// } else {
// None
// }
// })
// .ok_or_else(|| anyhow!("window was closed"))?;
// if let Some(task) = task {
// task.await?;
// }
// Ok(this)
// }))
// }
// fn add_event_to_update_proto(
// &self,
// event: &Self::Event,
// update: &mut Option<proto::update_view::Variant>,
// cx: &AppContext,
// ) -> bool {
// self.editor
// .read(cx)
// .add_event_to_update_proto(event, update, cx)
// }
// fn apply_update_proto(
// &mut self,
// project: &ModelHandle<Project>,
// message: proto::update_view::Variant,
// cx: &mut ViewContext<Self>,
// ) -> gpui::Task<anyhow::Result<()>> {
// self.editor.update(cx, |editor, cx| {
// editor.apply_update_proto(project, message, cx)
// })
// }
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
// self.editor.update(cx, |editor, cx| {
// editor.set_leader_peer_id(leader_peer_id, cx)
// })
// }
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
// Editor::should_unfollow_on_event(event, cx)
// }
// fn is_project_item(&self, _cx: &AppContext) -> bool {
// false
// }
// }
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
// impl CollaborationHub for ChannelBufferCollaborationHub {
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
// self.0.read(cx).collaborators()
// }
// fn user_participant_indices<'a>(
// &self,
// cx: &'a AppContext,
// ) -> &'a HashMap<u64, ParticipantIndex> {
// self.0.read(cx).user_store().read(cx).participant_indices()
// }
// }
+983
View File
@@ -0,0 +1,983 @@
// use crate::{
// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
// };
// use anyhow::Result;
// use call::ActiveCall;
// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
// use client::Client;
// use collections::HashMap;
// use db::kvp::KEY_VALUE_STORE;
// use editor::Editor;
// use gpui::{
// actions,
// elements::*,
// platform::{CursorStyle, MouseButton},
// serde_json,
// views::{ItemType, Select, SelectStyle},
// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
// ViewContext, ViewHandle, WeakViewHandle,
// };
// use language::LanguageRegistry;
// use menu::Confirm;
// use message_editor::MessageEditor;
// use project::Fs;
// use rich_text::RichText;
// use serde::{Deserialize, Serialize};
// use settings::SettingsStore;
// use std::sync::Arc;
// use theme::{IconButton, Theme};
// use time::{OffsetDateTime, UtcOffset};
// use util::{ResultExt, TryFutureExt};
// use workspace::{
// dock::{DockPosition, Panel},
// Workspace,
// };
// mod message_editor;
// const MESSAGE_LOADING_THRESHOLD: usize = 50;
// const CHAT_PANEL_KEY: &'static str = "ChatPanel";
// pub struct ChatPanel {
// client: Arc<Client>,
// channel_store: ModelHandle<ChannelStore>,
// languages: Arc<LanguageRegistry>,
// active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
// message_list: ListState<ChatPanel>,
// input_editor: ViewHandle<MessageEditor>,
// channel_select: ViewHandle<Select>,
// local_timezone: UtcOffset,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// is_scrolled_to_bottom: bool,
// has_focus: bool,
// markdown_data: HashMap<ChannelMessageId, RichText>,
// }
// #[derive(Serialize, Deserialize)]
// struct SerializedChatPanel {
// width: Option<f32>,
// }
// #[derive(Debug)]
// pub enum Event {
// DockPositionChanged,
// Focus,
// Dismissed,
// }
// actions!(
// chat_panel,
// [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
// );
// pub fn init(cx: &mut AppContext) {
// cx.add_action(ChatPanel::send);
// cx.add_action(ChatPanel::load_more_messages);
// cx.add_action(ChatPanel::open_notes);
// cx.add_action(ChatPanel::join_call);
// }
// impl ChatPanel {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
// let fs = workspace.app_state().fs.clone();
// let client = workspace.app_state().client.clone();
// let channel_store = ChannelStore::global(cx);
// let languages = workspace.app_state().languages.clone();
// let input_editor = cx.add_view(|cx| {
// MessageEditor::new(
// languages.clone(),
// channel_store.clone(),
// cx.add_view(|cx| {
// Editor::auto_height(
// 4,
// Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
// cx,
// )
// }),
// cx,
// )
// });
// let workspace_handle = workspace.weak_handle();
// let channel_select = cx.add_view(|cx| {
// let channel_store = channel_store.clone();
// let workspace = workspace_handle.clone();
// Select::new(0, cx, {
// move |ix, item_type, is_hovered, cx| {
// Self::render_channel_name(
// &channel_store,
// ix,
// item_type,
// is_hovered,
// workspace,
// cx,
// )
// }
// })
// .with_style(move |cx| {
// let style = &theme::current(cx).chat_panel.channel_select;
// SelectStyle {
// header: Default::default(),
// menu: style.menu,
// }
// })
// });
// let mut message_list =
// ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
// this.render_message(ix, cx)
// });
// message_list.set_scroll_handler(|visible_range, count, this, cx| {
// if visible_range.start < MESSAGE_LOADING_THRESHOLD {
// this.load_more_messages(&LoadMoreMessages, cx);
// }
// this.is_scrolled_to_bottom = visible_range.end == count;
// });
// cx.add_view(|cx| {
// let mut this = Self {
// fs,
// client,
// channel_store,
// languages,
// active_chat: Default::default(),
// pending_serialization: Task::ready(None),
// message_list,
// input_editor,
// channel_select,
// local_timezone: cx.platform().local_timezone(),
// has_focus: false,
// subscriptions: Vec::new(),
// workspace: workspace_handle,
// is_scrolled_to_bottom: true,
// active: false,
// width: None,
// markdown_data: Default::default(),
// };
// let mut old_dock_position = this.position(cx);
// this.subscriptions
// .push(
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
// let new_dock_position = this.position(cx);
// if new_dock_position != old_dock_position {
// old_dock_position = new_dock_position;
// cx.emit(Event::DockPositionChanged);
// }
// cx.notify();
// }),
// );
// this.update_channel_count(cx);
// cx.observe(&this.channel_store, |this, _, cx| {
// this.update_channel_count(cx)
// })
// .detach();
// cx.observe(&this.channel_select, |this, channel_select, cx| {
// let selected_ix = channel_select.read(cx).selected_index();
// let selected_channel_id = this
// .channel_store
// .read(cx)
// .channel_at(selected_ix)
// .map(|e| e.id);
// if let Some(selected_channel_id) = selected_channel_id {
// this.select_channel(selected_channel_id, None, cx)
// .detach_and_log_err(cx);
// }
// })
// .detach();
// this
// })
// }
// pub fn is_scrolled_to_bottom(&self) -> bool {
// self.is_scrolled_to_bottom
// }
// pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
// self.active_chat.as_ref().map(|(chat, _)| chat.clone())
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
// } else {
// None
// };
// workspace.update(&mut cx, |workspace, cx| {
// let panel = Self::new(workspace, cx);
// if let Some(serialized_panel) = serialized_panel {
// panel.update(cx, |panel, cx| {
// panel.width = serialized_panel.width;
// cx.notify();
// });
// }
// panel
// })
// })
// }
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
// let width = self.width;
// self.pending_serialization = cx.background().spawn(
// async move {
// KEY_VALUE_STORE
// .write_kvp(
// CHAT_PANEL_KEY.into(),
// serde_json::to_string(&SerializedChatPanel { width })?,
// )
// .await?;
// anyhow::Ok(())
// }
// .log_err(),
// );
// }
// fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
// let channel_count = self.channel_store.read(cx).channel_count();
// self.channel_select.update(cx, |select, cx| {
// select.set_item_count(channel_count, cx);
// });
// }
// fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
// if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
// let channel_id = chat.read(cx).channel_id;
// {
// self.markdown_data.clear();
// let chat = chat.read(cx);
// self.message_list.reset(chat.message_count());
// let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
// self.input_editor.update(cx, |editor, cx| {
// editor.set_channel(channel_id, channel_name, cx);
// });
// };
// let subscription = cx.subscribe(&chat, Self::channel_did_change);
// self.active_chat = Some((chat, subscription));
// self.acknowledge_last_message(cx);
// self.channel_select.update(cx, |select, cx| {
// if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
// select.set_selected_index(ix, cx);
// }
// });
// cx.notify();
// }
// }
// fn channel_did_change(
// &mut self,
// _: ModelHandle<ChannelChat>,
// event: &ChannelChatEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// ChannelChatEvent::MessagesUpdated {
// old_range,
// new_count,
// } => {
// self.message_list.splice(old_range.clone(), *new_count);
// if self.active {
// self.acknowledge_last_message(cx);
// }
// }
// ChannelChatEvent::NewMessage {
// channel_id,
// message_id,
// } => {
// if !self.active {
// self.channel_store.update(cx, |store, cx| {
// store.new_message(*channel_id, *message_id, cx)
// })
// }
// }
// }
// cx.notify();
// }
// fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
// if self.active && self.is_scrolled_to_bottom {
// if let Some((chat, _)) = &self.active_chat {
// chat.update(cx, |chat, cx| {
// chat.acknowledge_last_message(cx);
// });
// }
// }
// }
// fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// Flex::column()
// .with_child(
// ChildView::new(&self.channel_select, cx)
// .contained()
// .with_style(theme.chat_panel.channel_select.container),
// )
// .with_child(self.render_active_channel_messages(&theme))
// .with_child(self.render_input_box(&theme, cx))
// .into_any()
// }
// fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
// let messages = if self.active_chat.is_some() {
// List::new(self.message_list.clone())
// .contained()
// .with_style(theme.chat_panel.list)
// .into_any()
// } else {
// Empty::new().into_any()
// };
// messages.flex(1., true).into_any()
// }
// fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let (message, is_continuation, is_last, is_admin) = self
// .active_chat
// .as_ref()
// .unwrap()
// .0
// .update(cx, |active_chat, cx| {
// let is_admin = self
// .channel_store
// .read(cx)
// .is_channel_admin(active_chat.channel_id);
// let last_message = active_chat.message(ix.saturating_sub(1));
// let this_message = active_chat.message(ix).clone();
// let is_continuation = last_message.id != this_message.id
// && this_message.sender.id == last_message.sender.id;
// if let ChannelMessageId::Saved(id) = this_message.id {
// if this_message
// .mentions
// .iter()
// .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
// {
// active_chat.acknowledge_message(id);
// }
// }
// (
// this_message,
// is_continuation,
// active_chat.message_count() == ix + 1,
// is_admin,
// )
// });
// let is_pending = message.is_pending();
// let theme = theme::current(cx);
// let text = self.markdown_data.entry(message.id).or_insert_with(|| {
// Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
// });
// let now = OffsetDateTime::now_utc();
// let style = if is_pending {
// &theme.chat_panel.pending_message
// } else if is_continuation {
// &theme.chat_panel.continuation_message
// } else {
// &theme.chat_panel.message
// };
// let belongs_to_user = Some(message.sender.id) == self.client.user_id();
// let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
// (message.id, belongs_to_user || is_admin)
// {
// Some(id)
// } else {
// None
// };
// enum MessageBackgroundHighlight {}
// MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
// let container = style.style_for(state);
// if is_continuation {
// Flex::row()
// .with_child(
// text.element(
// theme.editor.syntax.clone(),
// theme.chat_panel.rich_text.clone(),
// cx,
// )
// .flex(1., true),
// )
// .with_child(render_remove(message_id_to_remove, cx, &theme))
// .contained()
// .with_style(*container)
// .with_margin_bottom(if is_last {
// theme.chat_panel.last_message_bottom_spacing
// } else {
// 0.
// })
// .into_any()
// } else {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(
// Flex::row()
// .with_child(render_avatar(
// message.sender.avatar.clone(),
// &theme.chat_panel.avatar,
// theme.chat_panel.avatar_container,
// ))
// .with_child(
// Label::new(
// message.sender.github_login.clone(),
// theme.chat_panel.message_sender.text.clone(),
// )
// .contained()
// .with_style(theme.chat_panel.message_sender.container),
// )
// .with_child(
// Label::new(
// format_timestamp(
// message.timestamp,
// now,
// self.local_timezone,
// ),
// theme.chat_panel.message_timestamp.text.clone(),
// )
// .contained()
// .with_style(theme.chat_panel.message_timestamp.container),
// )
// .align_children_center()
// .flex(1., true),
// )
// .with_child(render_remove(message_id_to_remove, cx, &theme))
// .align_children_center(),
// )
// .with_child(
// Flex::row()
// .with_child(
// text.element(
// theme.editor.syntax.clone(),
// theme.chat_panel.rich_text.clone(),
// cx,
// )
// .flex(1., true),
// )
// // Add a spacer to make everything line up
// .with_child(render_remove(None, cx, &theme)),
// )
// .contained()
// .with_style(*container)
// .with_margin_bottom(if is_last {
// theme.chat_panel.last_message_bottom_spacing
// } else {
// 0.
// })
// .into_any()
// }
// })
// .into_any()
// }
// fn render_markdown_with_mentions(
// language_registry: &Arc<LanguageRegistry>,
// current_user_id: u64,
// message: &channel::ChannelMessage,
// ) -> RichText {
// let mentions = message
// .mentions
// .iter()
// .map(|(range, user_id)| rich_text::Mention {
// range: range.clone(),
// is_self_mention: *user_id == current_user_id,
// })
// .collect::<Vec<_>>();
// rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
// }
// fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
// ChildView::new(&self.input_editor, cx)
// .contained()
// .with_style(theme.chat_panel.input_editor.container)
// .into_any()
// }
// fn render_channel_name(
// channel_store: &ModelHandle<ChannelStore>,
// ix: usize,
// item_type: ItemType,
// is_hovered: bool,
// workspace: WeakViewHandle<Workspace>,
// cx: &mut ViewContext<Select>,
// ) -> AnyElement<Select> {
// let theme = theme::current(cx);
// let tooltip_style = &theme.tooltip;
// let theme = &theme.chat_panel;
// let style = match (&item_type, is_hovered) {
// (ItemType::Header, _) => &theme.channel_select.header,
// (ItemType::Selected, _) => &theme.channel_select.active_item,
// (ItemType::Unselected, false) => &theme.channel_select.item,
// (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
// };
// let channel = &channel_store.read(cx).channel_at(ix).unwrap();
// let channel_id = channel.id;
// let mut row = Flex::row()
// .with_child(
// Label::new("#".to_string(), style.hash.text.clone())
// .contained()
// .with_style(style.hash.container),
// )
// .with_child(Label::new(channel.name.clone(), style.name.clone()));
// if matches!(item_type, ItemType::Header) {
// row.add_children([
// MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
// render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
// })
// .on_click(MouseButton::Left, move |_, _, cx| {
// if let Some(workspace) = workspace.upgrade(cx) {
// ChannelView::open(channel_id, workspace, cx).detach();
// }
// })
// .with_tooltip::<OpenChannelNotes>(
// channel_id as usize,
// "Open Notes",
// Some(Box::new(OpenChannelNotes)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
// render_icon_button(
// theme.icon_button.style_for(mouse_state),
// "icons/speaker-loud.svg",
// )
// })
// .on_click(MouseButton::Left, move |_, _, cx| {
// ActiveCall::global(cx)
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
// .detach_and_log_err(cx);
// })
// .with_tooltip::<ActiveCall>(
// channel_id as usize,
// "Join Call",
// Some(Box::new(JoinCall)),
// tooltip_style.clone(),
// cx,
// )
// .flex_float(),
// ]);
// }
// row.align_children_center()
// .contained()
// .with_style(style.container)
// .into_any()
// }
// fn render_sign_in_prompt(
// &self,
// theme: &Arc<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
// Label::new(
// "Sign in to use chat".to_string(),
// theme
// .chat_panel
// .sign_in_prompt
// .style_for(mouse_state)
// .clone(),
// )
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// let client = this.client.clone();
// cx.spawn(|this, mut cx| async move {
// if client
// .authenticate_and_connect(true, &cx)
// .log_err()
// .await
// .is_some()
// {
// this.update(&mut cx, |this, cx| {
// if cx.handle().is_focused(cx) {
// cx.focus(&this.input_editor);
// }
// })
// .ok();
// }
// })
// .detach();
// })
// .aligned()
// .into_any()
// }
// fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = self.active_chat.as_ref() {
// let message = self
// .input_editor
// .update(cx, |editor, cx| editor.take_message(cx));
// if let Some(task) = chat
// .update(cx, |chat, cx| chat.send_message(message, cx))
// .log_err()
// {
// task.detach();
// }
// }
// }
// fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = self.active_chat.as_ref() {
// chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
// }
// }
// fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = self.active_chat.as_ref() {
// chat.update(cx, |channel, cx| {
// if let Some(task) = channel.load_more_messages(cx) {
// task.detach();
// }
// })
// }
// }
// pub fn select_channel(
// &mut self,
// selected_channel_id: u64,
// scroll_to_message_id: Option<u64>,
// cx: &mut ViewContext<ChatPanel>,
// ) -> Task<Result<()>> {
// let open_chat = self
// .active_chat
// .as_ref()
// .and_then(|(chat, _)| {
// (chat.read(cx).channel_id == selected_channel_id)
// .then(|| Task::ready(anyhow::Ok(chat.clone())))
// })
// .unwrap_or_else(|| {
// self.channel_store.update(cx, |store, cx| {
// store.open_channel_chat(selected_channel_id, cx)
// })
// });
// cx.spawn(|this, mut cx| async move {
// let chat = open_chat.await?;
// this.update(&mut cx, |this, cx| {
// this.set_active_chat(chat.clone(), cx);
// })?;
// if let Some(message_id) = scroll_to_message_id {
// if let Some(item_ix) =
// ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
// .await
// {
// this.update(&mut cx, |this, cx| {
// if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
// this.message_list.scroll_to(ListOffset {
// item_ix,
// offset_in_item: 0.,
// });
// cx.notify();
// }
// })?;
// }
// }
// Ok(())
// })
// }
// fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = &self.active_chat {
// let channel_id = chat.read(cx).channel_id;
// if let Some(workspace) = self.workspace.upgrade(cx) {
// ChannelView::open(channel_id, workspace, cx).detach();
// }
// }
// }
// fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
// if let Some((chat, _)) = &self.active_chat {
// let channel_id = chat.read(cx).channel_id;
// ActiveCall::global(cx)
// .update(cx, |call, cx| call.join_channel(channel_id, cx))
// .detach_and_log_err(cx);
// }
// }
// }
// fn render_remove(
// message_id_to_remove: Option<u64>,
// cx: &mut ViewContext<'_, '_, ChatPanel>,
// theme: &Arc<Theme>,
// ) -> AnyElement<ChatPanel> {
// enum DeleteMessage {}
// message_id_to_remove
// .map(|id| {
// MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
// let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
// render_icon_button(button_style, "icons/x.svg")
// .aligned()
// .into_any()
// })
// .with_padding(Padding::uniform(2.))
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.remove_message(id, cx);
// })
// .flex_float()
// .into_any()
// })
// .unwrap_or_else(|| {
// let style = theme.chat_panel.icon_button.default;
// Empty::new()
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// .contained()
// .with_uniform_padding(2.)
// .flex_float()
// .into_any()
// })
// }
// impl Entity for ChatPanel {
// type Event = Event;
// }
// impl View for ChatPanel {
// fn ui_name() -> &'static str {
// "ChatPanel"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// let element = if self.client.user_id().is_some() {
// self.render_channel(cx)
// } else {
// self.render_sign_in_prompt(&theme, cx)
// };
// element
// .contained()
// .with_style(theme.chat_panel.container)
// .constrained()
// .with_min_width(150.)
// .into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// self.has_focus = true;
// if matches!(
// *self.client.status().borrow(),
// client::Status::Connected { .. }
// ) {
// let editor = self.input_editor.read(cx).editor.clone();
// cx.focus(&editor);
// }
// }
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = false;
// }
// }
// impl Panel for ChatPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<ChatPanelSettings>(cx).dock
// }
// fn position_is_valid(&self, position: DockPosition) -> bool {
// matches!(position, DockPosition::Left | DockPosition::Right)
// }
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
// settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
// settings.dock = Some(position)
// });
// }
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
// self.width = size;
// self.serialize(cx);
// cx.notify();
// }
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
// self.active = active;
// if active {
// self.acknowledge_last_message(cx);
// if !is_channels_feature_enabled(cx) {
// cx.emit(Event::Dismissed);
// }
// }
// }
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
// (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
// .then(|| "icons/conversations.svg")
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
// }
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
// fn should_close_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::Dismissed)
// }
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
// self.has_focus
// }
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// }
// }
// fn format_timestamp(
// mut timestamp: OffsetDateTime,
// mut now: OffsetDateTime,
// local_timezone: UtcOffset,
// ) -> String {
// timestamp = timestamp.to_offset(local_timezone);
// now = now.to_offset(local_timezone);
// let today = now.date();
// let date = timestamp.date();
// let mut hour = timestamp.hour();
// let mut part = "am";
// if hour > 12 {
// hour -= 12;
// part = "pm";
// }
// if date == today {
// format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
// } else if date.next_day() == Some(today) {
// format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
// } else {
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
// }
// }
// fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
// Svg::new(svg_path)
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// .contained()
// .with_style(style.container)
// }
// #[cfg(test)]
// mod tests {
// use super::*;
// use gpui::fonts::HighlightStyle;
// use pretty_assertions::assert_eq;
// use rich_text::{BackgroundKind, Highlight, RenderedRegion};
// use util::test::marked_text_ranges;
// #[gpui::test]
// fn test_render_markdown_with_mentions() {
// let language_registry = Arc::new(LanguageRegistry::test());
// let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
// let message = channel::ChannelMessage {
// id: ChannelMessageId::Saved(0),
// body,
// timestamp: OffsetDateTime::now_utc(),
// sender: Arc::new(client::User {
// github_login: "fgh".into(),
// avatar: None,
// id: 103,
// }),
// nonce: 5,
// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
// };
// let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
// // Note that the "'" was replaced with due to smart punctuation.
// let (body, ranges) = marked_text_ranges("«hi», «@abc», lets «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
// },
// ]
// );
// }
// }
@@ -0,0 +1,313 @@
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
lazy_static! {
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
"@[-_\\w]+",
false,
false,
Default::default(),
Default::default()
)
.unwrap();
}
pub struct MessageEditor {
pub editor: ViewHandle<Editor>,
channel_store: ModelHandle<ChannelStore>,
users: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
}
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
channel_store: ModelHandle<ChannelStore>,
editor: ViewHandle<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("message editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.app_context()
.spawn(|mut cx| async move {
let markdown = markdown.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
Self {
editor,
channel_store,
users: HashMap::default(),
channel_id: None,
mentions: Vec::new(),
mentions_task: None,
}
}
pub fn set_channel(
&mut self,
channel_id: u64,
channel_name: Option<String>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
if let Some(channel_name) = channel_name {
editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
} else {
editor.set_placeholder_text(format!("Message Channel"), cx);
}
});
self.channel_id = Some(channel_id);
self.refresh_users(cx);
}
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
if let Some(channel_id) = self.channel_id {
let members = self.channel_store.update(cx, |store, cx| {
store.get_channel_member_details(channel_id, cx)
});
cx.spawn(|this, mut cx| async move {
let members = members.await?;
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
self.users.clear();
self.users.extend(
members
.into_iter()
.map(|member| (member.user.github_login.clone(), member.user.id)),
);
}
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
self.editor.update(cx, |editor, cx| {
let highlights = editor.text_highlights::<Self>(cx);
let text = editor.text(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let mentions = if let Some((_, ranges)) = highlights {
ranges
.iter()
.map(|range| range.to_offset(&snapshot))
.zip(self.mentions.iter().copied())
.collect()
} else {
Vec::new()
};
editor.clear(cx);
self.mentions.clear();
MessageParams { text, mentions }
})
}
fn on_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
event: &language::Event,
cx: &mut ViewContext<Self>,
) {
if let language::Event::Reparsed | language::Event::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
Self::find_mentions(this, buffer, cx).await;
}));
}
}
async fn find_mentions(
this: WeakViewHandle<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncAppContext,
) {
let (buffer, ranges) = cx
.background()
.spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges)
})
.await;
this.update(&mut cx, |this, cx| {
let mut anchor_ranges = Vec::new();
let mut mentioned_user_ids = Vec::new();
let mut text = String::new();
this.editor.update(cx, |editor, cx| {
let multi_buffer = editor.buffer().read(cx).snapshot(cx);
for range in ranges {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix("@") {
if let Some(user_id) = this.users.get(username) {
let start = multi_buffer.anchor_after(range.start);
let end = multi_buffer.anchor_after(range.end);
mentioned_user_ids.push(*user_id);
anchor_ranges.push(start..end);
}
}
}
editor.clear_highlights::<Self>(cx);
editor.highlight_text::<Self>(
anchor_ranges,
theme::current(cx).chat_panel.rich_text.mention_highlight,
cx,
)
});
this.mentions = mentioned_user_ids;
this.mentions_task.take();
})
.ok();
}
}
impl Entity for MessageEditor {
type Event = ();
}
impl View for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{Client, User, UserStore};
use gpui::{TestAppContext, WindowHandle};
use language::{Language, LanguageConfig};
use rpc::proto;
use settings::SettingsStore;
use util::{http::FakeHttpClient, test::marked_text_ranges};
#[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) {
let editor = init_test(cx);
let editor = editor.root(cx);
editor.update(cx, |editor, cx| {
editor.set_members(
vec![
ChannelMembership {
user: Arc::new(User {
github_login: "a-b".into(),
id: 101,
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
ChannelMembership {
user: Arc::new(User {
github_login: "C_D".into(),
id: 102,
avatar: None,
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
],
cx,
);
editor.editor.update(cx, |editor, cx| {
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
});
});
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
editor.update(cx, |editor, cx| {
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
assert_eq!(
editor.take_message(cx),
MessageParams {
text,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
}
);
});
}
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
cx.foreground().forbid_parking();
cx.update(|cx| {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);
channel::init(&client, user_store, cx);
});
let language_registry = Arc::new(LanguageRegistry::test());
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
..Default::default()
},
Some(tree_sitter_markdown::language()),
)));
let editor = cx.add_window(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
cx,
)
});
cx.foreground().run_until_parked();
editor
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,717 @@
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
actions!(
channel_modal,
[
SelectNextControl,
ToggleMode,
ToggleMemberAdmin,
RemoveMember
]
);
pub fn init(cx: &mut AppContext) {
Picker::<ChannelModalDelegate>::init(cx);
cx.add_action(ChannelModal::toggle_mode);
cx.add_action(ChannelModal::toggle_member_admin);
cx.add_action(ChannelModal::remove_member);
cx.add_action(ChannelModal::dismiss);
}
pub struct ChannelModal {
picker: ViewHandle<Picker<ChannelModalDelegate>>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl ChannelModal {
pub fn new(
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
let picker = cx.add_view(|cx| {
Picker::new(
ChannelModalDelegate {
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
user_store: user_store.clone(),
channel_store: channel_store.clone(),
channel_id,
match_candidates: Vec::new(),
members,
mode,
context_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx.view_id(), cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
let has_focus = picker.read(cx).has_focus();
Self {
picker,
channel_store,
channel_id,
has_focus,
}
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
let mode = match self.picker.read(cx).delegate().mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
self.set_mode(mode, cx);
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate_mut().members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
});
cx.notify()
})
})
.detach();
}
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().toggle_selected_member_admin(cx);
})
}
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.delegate_mut().remove_selected_member(cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ChannelModal {
type Event = PickerEvent;
}
impl View for ChannelModal {
fn ui_name() -> &'static str {
"ChannelModal"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).collab_panel.tabbed_modal;
let mode = self.picker.read(cx).delegate().mode;
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
return Empty::new().into_any();
};
enum InviteMembers {}
enum ManageMembers {}
fn render_mode_button<T: 'static>(
mode: Mode,
text: &'static str,
current_mode: Mode,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
let active = mode == current_mode;
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
let contained_text = theme.tab_button.style_for(active, state);
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if !active {
this.set_mode(mode, cx);
}
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
}
fn render_visibility(
channel_id: ChannelId,
visibility: ChannelVisibility,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
enum TogglePublic {}
if visibility == ChannelVisibility::Members {
return Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Public,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any();
}
Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: ON"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Members,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.with_spacing(14.0)
.with_child(
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
let style = theme.channel_link.style_for(state);
Label::new(format!("{}", "copy link"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(channel) =
this.channel_store.read(cx).channel_for_id(channel_id)
{
let item = ClipboardItem::new(channel.link());
cx.write_to_clipboard(item);
}
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new(format!("#{}", channel.name), theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
.with_child(Flex::row().with_children([
render_mode_button::<InviteMembers>(
Mode::InviteMembers,
"Invite members",
mode,
theme,
cx,
),
render_mode_button::<ManageMembers>(
Mode::ManageMembers,
"Manage members",
mode,
theme,
cx,
),
]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ChannelModal {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum Mode {
ManageMembers,
InviteMembers,
}
pub struct ChannelModalDelegate {
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
user_store: ModelHandle<UserStore>,
channel_store: ModelHandle<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
context_menu: ViewHandle<ContextMenu>,
}
impl PickerDelegate for ChannelModalDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.len(),
Mode::InviteMembers => self.matching_users.len(),
}
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
}));
let matches = cx.background().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
cx.background().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
})
.ok();
})
}
Mode::InviteMembers => {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
let delegate = picker.delegate_mut();
delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
match self.mode {
Mode::ManageMembers => {
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
}
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_selected_member(cx);
}
Some(proto::channel_member::Kind::AncestorMember) | None => {
self.invite_member(selected_user, cx)
}
Some(proto::channel_member::Kind::Member) => {}
},
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.channel_modal;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let (user, role) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx);
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
let in_manage = matches!(self.mode, Mode::ManageMembers);
let mut result = 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(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children({
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|| {
Label::new("Invited", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
},
)
})
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
Some(
Label::new("Admin", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else if in_manage && role == Some(ChannelRole::Guest) {
Some(
Label::new("Guest", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else {
None
})
.with_children({
let svg = match self.mode {
Mode::ManageMembers => Some(
Svg::new("icons/ellipsis.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Mode::InviteMembers => match request_status {
Some(proto::channel_member::Kind::Member) => Some(
Svg::new("icons/check.svg")
.with_color(theme.member_icon.color)
.constrained()
.with_width(theme.member_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.member_icon.button_width)
.with_height(theme.member_icon.button_width)
.contained()
.with_style(theme.member_icon.container),
),
Some(proto::channel_member::Kind::Invitee) => Some(
Svg::new("icons/check.svg")
.with_color(theme.invitee_icon.color)
.constrained()
.with_width(theme.invitee_icon.icon_width)
.aligned()
.constrained()
.with_width(theme.invitee_icon.button_width)
.with_height(theme.invitee_icon.button_width)
.contained()
.with_style(theme.invitee_icon.container),
),
Some(proto::channel_member::Kind::AncestorMember) | None => None,
},
};
svg.map(|svg| svg.aligned().flex_float().into_any())
})
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any();
if selected {
result = Stack::new()
.with_child(result)
.with_child(
ChildView::new(&self.context_menu, cx)
.aligned()
.top()
.right(),
)
.into_any();
}
result
}
}
impl ChannelModalDelegate {
fn member_status(
&self,
user_id: UserId,
cx: &AppContext,
) -> Option<proto::channel_member::Kind> {
self.members
.iter()
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
.or_else(|| {
self.channel_store
.read(cx)
.has_pending_channel_invite(self.channel_id, user_id)
.then_some(proto::channel_member::Kind::Invitee)
})
}
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
let channel_membership = self.members.get(*ix)?;
Some((
channel_membership.user.clone(),
Some(channel_membership.role),
))
}),
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
}
}
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, role) = self.user_at_index(self.selected_index)?;
let new_role = if role == Some(ChannelRole::Admin) {
ChannelRole::Member
} else {
ChannelRole::Admin
};
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_role(self.channel_id, user.id, new_role, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.role = new_role;
}
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, _) = self.user_at_index(self.selected_index)?;
let user_id = user.id;
let update = self.channel_store.update(cx, |store, cx| {
store.remove_member(self.channel_id, user_id, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
this.matching_member_indices.retain_mut(|member_ix| {
if *member_ix == ix {
return false;
} else if *member_ix > ix {
*member_ix -= 1;
}
true
})
}
this.selected_index = this
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
cx.focus_self();
cx.notify();
})
})
.detach_and_log_err(cx);
Some(())
}
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
let members = &mut this.delegate_mut().members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
self.context_menu.update(cx, |context_menu, cx| {
context_menu.show(
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::action("Remove", RemoveMember),
ContextMenuItem::action(
if role == ChannelRole::Admin {
"Make non-admin"
} else {
"Make admin"
},
ToggleMemberAdmin,
),
],
cx,
)
})
}
}
@@ -0,0 +1,261 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Modal;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinderDelegate>::init(cx);
cx.add_action(ContactFinder::dismiss)
}
pub struct ContactFinder {
picker: ViewHandle<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.add_view(|cx| {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
Self {
picker,
has_focus: false,
}
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
picker.set_query(query, cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
}
}
impl Entity for ContactFinder {
type Event = PickerEvent;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.tabbed_modal;
fn render_mode_button(
text: &'static str,
theme: &theme::TabbedModal,
_cx: &mut ViewContext<ContactFinder>,
) -> AnyElement<ContactFinder> {
let contained_text = &theme.tab_button.active_state().default;
Label::new(text, contained_text.text.clone())
.contained()
.with_style(contained_text.container.clone())
.into_any()
}
Flex::column()
.with_child(
Flex::column()
.with_child(
Label::new("Contacts", theme.title.text.clone())
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(Flex::row().with_children([render_mode_button(
"Invite new contacts",
&theme,
cx,
)]))
.expanded()
.contained()
.with_style(theme.header),
)
.with_child(
ChildView::new(&self.picker, cx)
.contained()
.with_style(theme.body),
)
.constrained()
.with_max_height(theme.max_height)
.with_max_width(theme.max_width)
.contained()
.with_style(theme.modal)
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.picker)
}
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
self.has_focus = false;
}
}
impl Modal for ContactFinder {
fn has_focus(&self) -> bool {
self.has_focus
}
fn dismiss_on_event(event: &Self::Event) -> bool {
match event {
PickerEvent::Dismiss => true,
}
}
}
pub struct ContactFinderDelegate {
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
selected_index: usize,
}
impl PickerDelegate for ContactFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|picker, mut cx| async move {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate_mut().potential_contacts = potential_contacts.into();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
self.user_store
.update(cx, |store, cx| store.request_contact(user.id, cx))
.detach();
}
ContactRequestStatus::RequestSent => {
self.user_store
.update(cx, |store, cx| store.remove_contact(user.id, cx))
.detach();
}
_ => {}
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
&self,
ix: usize,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.contact_finder;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
Some("icons/check.svg")
}
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.disabled_contact_button
} else {
&theme.contact_button
};
let style = tabbed_modal
.picker
.item
.in_state(selected)
.style_for(mouse_state);
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(), style.label.clone())
.contained()
.with_style(theme.contact_username)
.aligned()
.left(),
)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned()
.flex_float()
}))
.contained()
.with_style(style.container)
.constrained()
.with_height(tabbed_modal.row_height)
.into_any()
}
}
File diff suppressed because it is too large Load Diff
+153
View File
@@ -0,0 +1,153 @@
pub mod channel_view;
pub mod chat_panel;
pub mod collab_panel;
mod collab_titlebar_item;
mod face_pile;
pub mod notification_panel;
pub mod notifications;
mod panel_settings;
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]
// );
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx);
// vcs_menu::init(cx);
collab_titlebar_item::init(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);
}
// 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);
// 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();
// }
// }
// fn notification_window_options(
// screen: Rc<dyn Screen>,
// window_size: Vector2F,
// ) -> WindowOptions<'static> {
// const NOTIFICATION_PADDING: f32 = 16.;
// let screen_bounds = screen.content_bounds();
// WindowOptions {
// bounds: WindowBounds::Fixed(RectF::new(
// screen_bounds.upper_right()
// + vec2f(
// -NOTIFICATION_PADDING - window_size.x(),
// NOTIFICATION_PADDING,
// ),
// window_size,
// )),
// titlebar: None,
// center: false,
// focus: false,
// show: true,
// kind: WindowKind::PopUp,
// is_movable: false,
// screen: Some(screen),
// }
// }
// fn render_avatar<T: 'static>(
// avatar: Option<Arc<ImageData>>,
// avatar_style: &AvatarStyle,
// container: ContainerStyle,
// ) -> AnyElement<T> {
// avatar
// .map(|avatar| {
// Image::from_data(avatar)
// .with_style(avatar_style.image)
// .aligned()
// .contained()
// .with_corner_radius(avatar_style.outer_corner_radius)
// .constrained()
// .with_width(avatar_style.outer_width)
// .with_height(avatar_style.outer_width)
// .into_any()
// })
// .unwrap_or_else(|| {
// Empty::new()
// .constrained()
// .with_width(avatar_style.outer_width)
// .into_any()
// })
// .contained()
// .with_style(container)
// .into_any()
// }
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
// }
+113
View File
@@ -0,0 +1,113 @@
// use std::ops::Range;
// use gpui::{
// geometry::{
// rect::RectF,
// vector::{vec2f, Vector2F},
// },
// json::ToJson,
// serde_json::{self, json},
// AnyElement, Axis, Element, View, ViewContext,
// };
// pub(crate) struct FacePile<V: View> {
// overlap: f32,
// faces: Vec<AnyElement<V>>,
// }
// impl<V: View> FacePile<V> {
// pub fn new(overlap: f32) -> Self {
// Self {
// overlap,
// faces: Vec::new(),
// }
// }
// }
// impl<V: View> Element<V> for FacePile<V> {
// type LayoutState = ();
// type PaintState = ();
// fn layout(
// &mut self,
// constraint: gpui::SizeConstraint,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> (Vector2F, Self::LayoutState) {
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
// let mut width = 0.;
// let mut max_height = 0.;
// for face in &mut self.faces {
// let layout = face.layout(constraint, view, cx);
// width += layout.x();
// max_height = f32::max(max_height, layout.y());
// }
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
// (
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
// (),
// )
// }
// fn paint(
// &mut self,
// bounds: RectF,
// visible_bounds: RectF,
// _layout: &mut Self::LayoutState,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> Self::PaintState {
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// 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;
// cx.scene().push_layer(None);
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
// cx.scene().pop_layer();
// origin_x += self.overlap;
// }
// ()
// }
// fn rect_for_text_range(
// &self,
// _: Range<usize>,
// _: RectF,
// _: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> Option<RectF> {
// None
// }
// fn debug(
// &self,
// bounds: RectF,
// _: &Self::LayoutState,
// _: &Self::PaintState,
// _: &V,
// _: &ViewContext<V>,
// ) -> serde_json::Value {
// json!({
// "type": "FacePile",
// "bounds": bounds.to_json()
// })
// }
// }
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
// self.faces.extend(children);
// }
// }
+884
View File
@@ -0,0 +1,884 @@
// use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
// use anyhow::Result;
// use channel::ChannelStore;
// use client::{Client, Notification, User, UserStore};
// use collections::HashMap;
// use db::kvp::KEY_VALUE_STORE;
// use futures::StreamExt;
// use gpui::{
// actions,
// elements::*,
// platform::{CursorStyle, MouseButton},
// serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
// ViewContext, ViewHandle, WeakViewHandle, WindowContext,
// };
// use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
// use project::Fs;
// use rpc::proto;
// use serde::{Deserialize, Serialize};
// use settings::SettingsStore;
// use std::{sync::Arc, time::Duration};
// use theme::{ui, Theme};
// use time::{OffsetDateTime, UtcOffset};
// use util::{ResultExt, TryFutureExt};
// use workspace::{
// dock::{DockPosition, Panel},
// Workspace,
// };
// const LOADING_THRESHOLD: usize = 30;
// const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
// const TOAST_DURATION: Duration = Duration::from_secs(5);
// const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
// pub struct NotificationPanel {
// client: Arc<Client>,
// user_store: ModelHandle<UserStore>,
// channel_store: ModelHandle<ChannelStore>,
// notification_store: ModelHandle<NotificationStore>,
// fs: Arc<dyn Fs>,
// width: Option<f32>,
// active: bool,
// notification_list: ListState<Self>,
// pending_serialization: Task<Option<()>>,
// subscriptions: Vec<gpui::Subscription>,
// workspace: WeakViewHandle<Workspace>,
// current_notification_toast: Option<(u64, Task<()>)>,
// local_timezone: UtcOffset,
// has_focus: bool,
// mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
// }
// #[derive(Serialize, Deserialize)]
// struct SerializedNotificationPanel {
// width: Option<f32>,
// }
// #[derive(Debug)]
// pub enum Event {
// DockPositionChanged,
// Focus,
// Dismissed,
// }
// pub struct NotificationPresenter {
// pub actor: Option<Arc<client::User>>,
// pub text: String,
// pub icon: &'static str,
// pub needs_response: bool,
// pub can_navigate: bool,
// }
// actions!(notification_panel, [ToggleFocus]);
// pub fn init(_cx: &mut AppContext) {}
// impl NotificationPanel {
// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
// let fs = workspace.app_state().fs.clone();
// let client = workspace.app_state().client.clone();
// let user_store = workspace.app_state().user_store.clone();
// let workspace_handle = workspace.weak_handle();
// cx.add_view(|cx| {
// let mut status = client.status();
// cx.spawn(|this, mut cx| async move {
// while let Some(_) = status.next().await {
// if this
// .update(&mut cx, |_, cx| {
// cx.notify();
// })
// .is_err()
// {
// break;
// }
// }
// })
// .detach();
// let mut notification_list =
// ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
// this.render_notification(ix, cx)
// .unwrap_or_else(|| Empty::new().into_any())
// });
// notification_list.set_scroll_handler(|visible_range, count, this, cx| {
// if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
// if let Some(task) = this
// .notification_store
// .update(cx, |store, cx| store.load_more_notifications(false, cx))
// {
// task.detach();
// }
// }
// });
// let mut this = Self {
// fs,
// client,
// user_store,
// local_timezone: cx.platform().local_timezone(),
// channel_store: ChannelStore::global(cx),
// notification_store: NotificationStore::global(cx),
// notification_list,
// pending_serialization: Task::ready(None),
// workspace: workspace_handle,
// has_focus: false,
// current_notification_toast: None,
// subscriptions: Vec::new(),
// active: false,
// mark_as_read_tasks: HashMap::default(),
// width: None,
// };
// let mut old_dock_position = this.position(cx);
// this.subscriptions.extend([
// cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
// cx.subscribe(&this.notification_store, Self::on_notification_event),
// cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
// let new_dock_position = this.position(cx);
// if new_dock_position != old_dock_position {
// old_dock_position = new_dock_position;
// cx.emit(Event::DockPositionChanged);
// }
// cx.notify();
// }),
// ]);
// this
// })
// }
// pub fn load(
// workspace: WeakViewHandle<Workspace>,
// cx: AsyncAppContext,
// ) -> Task<Result<ViewHandle<Self>>> {
// cx.spawn(|mut cx| async move {
// let serialized_panel = if let Some(panel) = cx
// .background()
// .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
// .await
// .log_err()
// .flatten()
// {
// Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
// } else {
// None
// };
// workspace.update(&mut cx, |workspace, cx| {
// let panel = Self::new(workspace, cx);
// if let Some(serialized_panel) = serialized_panel {
// panel.update(cx, |panel, cx| {
// panel.width = serialized_panel.width;
// cx.notify();
// });
// }
// panel
// })
// })
// }
// fn serialize(&mut self, cx: &mut ViewContext<Self>) {
// let width = self.width;
// self.pending_serialization = cx.background().spawn(
// async move {
// KEY_VALUE_STORE
// .write_kvp(
// NOTIFICATION_PANEL_KEY.into(),
// serde_json::to_string(&SerializedNotificationPanel { width })?,
// )
// .await?;
// anyhow::Ok(())
// }
// .log_err(),
// );
// }
// fn render_notification(
// &mut self,
// ix: usize,
// cx: &mut ViewContext<Self>,
// ) -> Option<AnyElement<Self>> {
// let entry = self.notification_store.read(cx).notification_at(ix)?;
// let notification_id = entry.id;
// let now = OffsetDateTime::now_utc();
// let timestamp = entry.timestamp;
// let NotificationPresenter {
// actor,
// text,
// needs_response,
// can_navigate,
// ..
// } = self.present_notification(entry, cx)?;
// let theme = theme::current(cx);
// let style = &theme.notification_panel;
// let response = entry.response;
// let notification = entry.notification.clone();
// let message_style = if entry.is_read {
// style.read_text.clone()
// } else {
// style.unread_text.clone()
// };
// if self.active && !entry.is_read {
// self.did_render_notification(notification_id, &notification, cx);
// }
// enum Decline {}
// enum Accept {}
// Some(
// MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
// let container = message_style.container;
// Flex::row()
// .with_children(actor.map(|actor| {
// render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
// }))
// .with_child(
// Flex::column()
// .with_child(Text::new(text, message_style.text.clone()))
// .with_child(
// Flex::row()
// .with_child(
// Label::new(
// format_timestamp(timestamp, now, self.local_timezone),
// style.timestamp.text.clone(),
// )
// .contained()
// .with_style(style.timestamp.container),
// )
// .with_children(if let Some(is_accepted) = response {
// Some(
// Label::new(
// if is_accepted {
// "You accepted"
// } else {
// "You declined"
// },
// style.read_text.text.clone(),
// )
// .flex_float()
// .into_any(),
// )
// } else if needs_response {
// Some(
// Flex::row()
// .with_children([
// MouseEventHandler::new::<Decline, _>(
// ix,
// cx,
// |state, _| {
// let button =
// style.button.style_for(state);
// Label::new(
// "Decline",
// button.text.clone(),
// )
// .contained()
// .with_style(button.container)
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, view, cx| {
// view.respond_to_notification(
// notification.clone(),
// false,
// cx,
// );
// }
// }),
// MouseEventHandler::new::<Accept, _>(
// ix,
// cx,
// |state, _| {
// let button =
// style.button.style_for(state);
// Label::new(
// "Accept",
// button.text.clone(),
// )
// .contained()
// .with_style(button.container)
// },
// )
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, view, cx| {
// view.respond_to_notification(
// notification.clone(),
// true,
// cx,
// );
// }
// }),
// ])
// .flex_float()
// .into_any(),
// )
// } else {
// None
// }),
// )
// .flex(1.0, true),
// )
// .contained()
// .with_style(container)
// .into_any()
// })
// .with_cursor_style(if can_navigate {
// CursorStyle::PointingHand
// } else {
// CursorStyle::default()
// })
// .on_click(MouseButton::Left, {
// let notification = notification.clone();
// move |_, this, cx| this.did_click_notification(&notification, cx)
// })
// .into_any(),
// )
// }
// fn present_notification(
// &self,
// entry: &NotificationEntry,
// cx: &AppContext,
// ) -> Option<NotificationPresenter> {
// let user_store = self.user_store.read(cx);
// let channel_store = self.channel_store.read(cx);
// match entry.notification {
// Notification::ContactRequest { sender_id } => {
// let requester = user_store.get_cached_user(sender_id)?;
// Some(NotificationPresenter {
// icon: "icons/plus.svg",
// text: format!("{} wants to add you as a contact", requester.github_login),
// needs_response: user_store.has_incoming_contact_request(requester.id),
// actor: Some(requester),
// can_navigate: false,
// })
// }
// Notification::ContactRequestAccepted { responder_id } => {
// let responder = user_store.get_cached_user(responder_id)?;
// Some(NotificationPresenter {
// icon: "icons/plus.svg",
// text: format!("{} accepted your contact invite", responder.github_login),
// needs_response: false,
// actor: Some(responder),
// can_navigate: false,
// })
// }
// Notification::ChannelInvitation {
// ref channel_name,
// channel_id,
// inviter_id,
// } => {
// let inviter = user_store.get_cached_user(inviter_id)?;
// Some(NotificationPresenter {
// icon: "icons/hash.svg",
// text: format!(
// "{} invited you to join the #{channel_name} channel",
// inviter.github_login
// ),
// needs_response: channel_store.has_channel_invitation(channel_id),
// actor: Some(inviter),
// can_navigate: false,
// })
// }
// Notification::ChannelMessageMention {
// sender_id,
// channel_id,
// message_id,
// } => {
// let sender = user_store.get_cached_user(sender_id)?;
// let channel = channel_store.channel_for_id(channel_id)?;
// let message = self
// .notification_store
// .read(cx)
// .channel_message_for_id(message_id)?;
// Some(NotificationPresenter {
// icon: "icons/conversations.svg",
// text: format!(
// "{} mentioned you in #{}:\n{}",
// sender.github_login, channel.name, message.body,
// ),
// needs_response: false,
// actor: Some(sender),
// can_navigate: true,
// })
// }
// }
// }
// fn did_render_notification(
// &mut self,
// notification_id: u64,
// notification: &Notification,
// cx: &mut ViewContext<Self>,
// ) {
// let should_mark_as_read = match notification {
// Notification::ContactRequestAccepted { .. } => true,
// Notification::ContactRequest { .. }
// | Notification::ChannelInvitation { .. }
// | Notification::ChannelMessageMention { .. } => false,
// };
// if should_mark_as_read {
// self.mark_as_read_tasks
// .entry(notification_id)
// .or_insert_with(|| {
// let client = self.client.clone();
// cx.spawn(|this, mut cx| async move {
// cx.background().timer(MARK_AS_READ_DELAY).await;
// client
// .request(proto::MarkNotificationRead { notification_id })
// .await?;
// this.update(&mut cx, |this, _| {
// this.mark_as_read_tasks.remove(&notification_id);
// })?;
// Ok(())
// })
// });
// }
// }
// fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
// if let Notification::ChannelMessageMention {
// message_id,
// channel_id,
// ..
// } = notification.clone()
// {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// cx.app_context().defer(move |cx| {
// workspace.update(cx, |workspace, cx| {
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// panel.update(cx, |panel, cx| {
// panel
// .select_channel(channel_id, Some(message_id), cx)
// .detach_and_log_err(cx);
// });
// }
// });
// });
// }
// }
// }
// fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
// if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
// if let Some(workspace) = self.workspace.upgrade(cx) {
// return workspace
// .read_with(cx, |workspace, cx| {
// if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
// return panel.read_with(cx, |panel, cx| {
// panel.is_scrolled_to_bottom()
// && panel.active_chat().map_or(false, |chat| {
// chat.read(cx).channel_id == *channel_id
// })
// });
// }
// false
// })
// .unwrap_or_default();
// }
// }
// false
// }
// fn render_sign_in_prompt(
// &self,
// theme: &Arc<Theme>,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum SignInPromptLabel {}
// MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
// Label::new(
// "Sign in to view your notifications".to_string(),
// theme
// .chat_panel
// .sign_in_prompt
// .style_for(mouse_state)
// .clone(),
// )
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// let client = this.client.clone();
// cx.spawn(|_, cx| async move {
// client.authenticate_and_connect(true, &cx).log_err().await;
// })
// .detach();
// })
// .aligned()
// .into_any()
// }
// fn render_empty_state(
// &self,
// theme: &Arc<Theme>,
// _cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// Label::new(
// "You have no notifications".to_string(),
// theme.chat_panel.sign_in_prompt.default.clone(),
// )
// .aligned()
// .into_any()
// }
// fn on_notification_event(
// &mut self,
// _: ModelHandle<NotificationStore>,
// event: &NotificationEvent,
// cx: &mut ViewContext<Self>,
// ) {
// match event {
// NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
// NotificationEvent::NotificationRemoved { entry }
// | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
// NotificationEvent::NotificationsUpdated {
// old_range,
// new_count,
// } => {
// self.notification_list.splice(old_range.clone(), *new_count);
// cx.notify();
// }
// }
// }
// fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
// if self.is_showing_notification(&entry.notification, cx) {
// return;
// }
// let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
// else {
// return;
// };
// let notification_id = entry.id;
// self.current_notification_toast = Some((
// notification_id,
// cx.spawn(|this, mut cx| async move {
// cx.background().timer(TOAST_DURATION).await;
// this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
// .ok();
// }),
// ));
// self.workspace
// .update(cx, |workspace, cx| {
// workspace.dismiss_notification::<NotificationToast>(0, cx);
// workspace.show_notification(0, cx, |cx| {
// let workspace = cx.weak_handle();
// cx.add_view(|_| NotificationToast {
// notification_id,
// actor,
// text,
// workspace,
// })
// })
// })
// .ok();
// }
// fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
// if let Some((current_id, _)) = &self.current_notification_toast {
// if *current_id == notification_id {
// self.current_notification_toast.take();
// self.workspace
// .update(cx, |workspace, cx| {
// workspace.dismiss_notification::<NotificationToast>(0, cx)
// })
// .ok();
// }
// }
// }
// fn respond_to_notification(
// &mut self,
// notification: Notification,
// response: bool,
// cx: &mut ViewContext<Self>,
// ) {
// self.notification_store.update(cx, |store, cx| {
// store.respond_to_notification(notification, response, cx);
// });
// }
// }
// impl Entity for NotificationPanel {
// type Event = Event;
// }
// impl View for NotificationPanel {
// fn ui_name() -> &'static str {
// "NotificationPanel"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let theme = theme::current(cx);
// let style = &theme.notification_panel;
// let element = if self.client.user_id().is_none() {
// self.render_sign_in_prompt(&theme, cx)
// } else if self.notification_list.item_count() == 0 {
// self.render_empty_state(&theme, cx)
// } else {
// Flex::column()
// .with_child(
// Flex::row()
// .with_child(Label::new("Notifications", style.title.text.clone()))
// .with_child(ui::svg(&style.title_icon).flex_float())
// .align_children_center()
// .contained()
// .with_style(style.title.container)
// .constrained()
// .with_height(style.title_height),
// )
// .with_child(
// List::new(self.notification_list.clone())
// .contained()
// .with_style(style.list)
// .flex(1., true),
// )
// .into_any()
// };
// element
// .contained()
// .with_style(style.container)
// .constrained()
// .with_min_width(150.)
// .into_any()
// }
// fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = true;
// }
// fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
// self.has_focus = false;
// }
// }
// impl Panel for NotificationPanel {
// fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
// settings::get::<NotificationPanelSettings>(cx).dock
// }
// fn position_is_valid(&self, position: DockPosition) -> bool {
// matches!(position, DockPosition::Left | DockPosition::Right)
// }
// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
// settings::update_settings_file::<NotificationPanelSettings>(
// self.fs.clone(),
// cx,
// move |settings| settings.dock = Some(position),
// );
// }
// fn size(&self, cx: &gpui::WindowContext) -> f32 {
// self.width
// .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
// }
// fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
// self.width = size;
// self.serialize(cx);
// cx.notify();
// }
// fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
// self.active = active;
// if self.notification_store.read(cx).notification_count() == 0 {
// cx.emit(Event::Dismissed);
// }
// }
// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
// (settings::get::<NotificationPanelSettings>(cx).button
// && self.notification_store.read(cx).notification_count() > 0)
// .then(|| "icons/bell.svg")
// }
// fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
// (
// "Notification Panel".to_string(),
// Some(Box::new(ToggleFocus)),
// )
// }
// fn icon_label(&self, cx: &WindowContext) -> Option<String> {
// let count = self.notification_store.read(cx).unread_notification_count();
// if count == 0 {
// None
// } else {
// Some(count.to_string())
// }
// }
// fn should_change_position_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::DockPositionChanged)
// }
// fn should_close_on_event(event: &Self::Event) -> bool {
// matches!(event, Event::Dismissed)
// }
// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
// self.has_focus
// }
// fn is_focus_event(event: &Self::Event) -> bool {
// matches!(event, Event::Focus)
// }
// }
// pub struct NotificationToast {
// notification_id: u64,
// actor: Option<Arc<User>>,
// text: String,
// workspace: WeakViewHandle<Workspace>,
// }
// pub enum ToastEvent {
// Dismiss,
// }
// impl NotificationToast {
// fn focus_notification_panel(&self, cx: &mut AppContext) {
// let workspace = self.workspace.clone();
// let notification_id = self.notification_id;
// cx.defer(move |cx| {
// workspace
// .update(cx, |workspace, cx| {
// if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
// panel.update(cx, |panel, cx| {
// let store = panel.notification_store.read(cx);
// if let Some(entry) = store.notification_for_id(notification_id) {
// panel.did_click_notification(&entry.clone().notification, cx);
// }
// });
// }
// })
// .ok();
// })
// }
// }
// impl Entity for NotificationToast {
// type Event = ToastEvent;
// }
// impl View for NotificationToast {
// fn ui_name() -> &'static str {
// "ContactNotification"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let user = self.actor.clone();
// let theme = theme::current(cx).clone();
// let theme = &theme.contact_notification;
// MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
// Flex::row()
// .with_children(user.and_then(|user| {
// Some(
// Image::from_data(user.avatar.clone()?)
// .with_style(theme.header_avatar)
// .aligned()
// .constrained()
// .with_height(
// cx.font_cache()
// .line_height(theme.header_message.text.font_size),
// )
// .aligned()
// .top(),
// )
// }))
// .with_child(
// Text::new(self.text.clone(), theme.header_message.text.clone())
// .contained()
// .with_style(theme.header_message.container)
// .aligned()
// .top()
// .left()
// .flex(1., true),
// )
// .with_child(
// MouseEventHandler::new::<ToastEvent, _>(0, cx, |state, _| {
// let style = theme.dismiss_button.style_for(state);
// Svg::new("icons/x.svg")
// .with_color(style.color)
// .constrained()
// .with_width(style.icon_width)
// .aligned()
// .contained()
// .with_style(style.container)
// .constrained()
// .with_width(style.button_width)
// .with_height(style.button_width)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .with_padding(Padding::uniform(5.))
// .on_click(MouseButton::Left, move |_, _, cx| {
// cx.emit(ToastEvent::Dismiss)
// })
// .aligned()
// .constrained()
// .with_height(
// cx.font_cache()
// .line_height(theme.header_message.text.font_size),
// )
// .aligned()
// .top()
// .flex_float(),
// )
// .contained()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, this, cx| {
// this.focus_notification_panel(cx);
// cx.emit(ToastEvent::Dismiss);
// })
// .into_any()
// }
// }
// impl workspace::notifications::Notification for NotificationToast {
// fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
// matches!(event, ToastEvent::Dismiss)
// }
// }
// fn format_timestamp(
// mut timestamp: OffsetDateTime,
// mut now: OffsetDateTime,
// local_timezone: UtcOffset,
// ) -> String {
// timestamp = timestamp.to_offset(local_timezone);
// now = now.to_offset(local_timezone);
// let today = now.date();
// let date = timestamp.date();
// if date == today {
// let difference = now - timestamp;
// if difference >= Duration::from_secs(3600) {
// format!("{}h", difference.whole_seconds() / 3600)
// } else if difference >= Duration::from_secs(60) {
// format!("{}m", difference.whole_seconds() / 60)
// } else {
// "just now".to_string()
// }
// } else if date.next_day() == Some(today) {
// format!("yesterday")
// } else {
// format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
// }
// }
+11
View File
@@ -0,0 +1,11 @@
// use gpui::AppContext;
// use std::sync::Arc;
// use workspace::AppState;
// pub mod incoming_call_notification;
// pub mod project_shared_notification;
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
// incoming_call_notification::init(app_state, cx);
// project_shared_notification::init(app_state, cx);
// }
@@ -0,0 +1,213 @@
use crate::notification_window_options;
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
};
use std::sync::{Arc, Weak};
use util::ResultExt;
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
while let Some(incoming_call) = incoming_call.next().await {
for window in notification_windows.drain(..) {
window.remove(&mut cx);
}
if let Some(incoming_call) = incoming_call {
let window_size = cx.read(|cx| {
let theme = &theme::current(cx).incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
for screen in cx.platform().screens() {
let window = cx
.add_window(notification_window_options(screen, window_size), |_| {
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
});
notification_windows.push(window);
}
}
}
})
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
pub struct IncomingCallNotification {
call: IncomingCall,
app_state: Weak<AppState>,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone();
cx.app_context()
.spawn(|mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, cx| {
active_call.decline_incoming(cx).log_err();
});
}
}
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
.initial_project
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.caller_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.calling_user.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Accept {}
enum Decline {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx);
})
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
let theme = &theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.incoming_call_notification.button_width)
.into_any()
}
}
impl Entity for IncomingCallNotification {
type Event = ();
}
impl View for IncomingCallNotification {
fn ui_name() -> &'static str {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = theme::current(cx).incoming_call_notification.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
}
}
@@ -0,0 +1,217 @@
use crate::notification_window_options;
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AppContext, Entity, View, ViewContext,
};
use std::sync::{Arc, Weak};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
room::Event::RemoteProjectShared {
owner,
project_id,
worktree_root_names,
} => {
let theme = &theme::current(cx).project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
let window =
cx.add_window(notification_window_options(screen, window_size), |_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
});
notification_windows
.entry(*project_id)
.or_insert(Vec::new())
.push(window);
}
}
room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows {
window.remove(cx);
}
}
}
room::Event::Left => {
for (_, windows) in notification_windows.drain() {
for window in windows {
window.remove(cx);
}
}
}
_ => {}
})
.detach();
}
pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
app_state: Weak<AppState>,
}
impl ProjectSharedNotification {
fn new(
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
app_state: Weak<AppState>,
) -> Self {
Self {
project_id,
worktree_root_names,
owner,
app_state,
}
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_room) =
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
{
active_room.update(cx, |_, cx| {
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
});
});
}
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container),
)
.with_children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Open {}
enum Dismiss {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.project_shared_notification.button_width)
.into_any()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = theme::current(cx).project_shared_notification.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
}
}
+69
View File
@@ -0,0 +1,69 @@
use anyhow;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
pub struct CollaborationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Deserialize, Debug)]
pub struct ChatPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct PanelSettingsContent {
pub button: Option<bool>,
pub dock: Option<DockPosition>,
pub default_width: Option<f32>,
}
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],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
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],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
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],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
+14 -87
View File
@@ -36,11 +36,10 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render,
Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext,
WindowHandle, WindowOptions,
actions, div, point, prelude::*, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
AsyncAppContext, AsyncWindowContext, Bounds, Div, Entity, EntityId, EventEmitter, GlobalPixels,
KeyContext, Model, ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription,
Task, View, ViewContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -68,8 +67,6 @@ use std::{
};
use theme2::{ActiveTheme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use ui::TextColor;
use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
use util::ResultExt;
use uuid::Uuid;
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
@@ -447,7 +444,7 @@ pub struct Workspace {
last_active_view_id: Option<proto::ViewId>,
status_bar: View<StatusBar>,
modal_layer: View<ModalLayer>,
// titlebar_item: Option<AnyViewHandle>,
titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
follower_states: HashMap<View<Pane>, FollowerState>,
@@ -660,7 +657,7 @@ impl Workspace {
last_active_view_id: None,
status_bar,
modal_layer,
// titlebar_item: None,
titlebar_item: None,
notifications: Default::default(),
left_dock,
bottom_dock,
@@ -1033,15 +1030,14 @@ impl Workspace {
&self.app_state.client
}
// todo!()
// pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
// self.titlebar_item = Some(item);
// cx.notify();
// }
pub fn set_titlebar_item(&mut self, item: AnyView, cx: &mut ViewContext<Self>) {
self.titlebar_item = Some(item);
cx.notify();
}
// pub fn titlebar_item(&self) -> Option<AnyViewHandle> {
// self.titlebar_item.clone()
// }
pub fn titlebar_item(&self) -> Option<AnyView> {
self.titlebar_item.clone()
}
/// Call the given callback with a workspace whose project is local.
///
@@ -2448,75 +2444,6 @@ impl Workspace {
// .any(|state| state.leader_id == peer_id)
// }
fn render_titlebar(&self, cx: &mut ViewContext<Self>) -> impl Component<Self> {
h_stack()
.id("titlebar")
.justify_between()
.when(
!matches!(cx.window_bounds(), WindowBounds::Fullscreen),
|s| s.pl_20(),
)
.w_full()
.h(rems(1.75))
.bg(cx.theme().colors().title_bar_background)
.on_click(|_, event, cx| {
if event.up.click_count == 2 {
cx.zoom_window();
}
})
.child(
h_stack()
// TODO - Add player menu
.child(
div()
.id("project_owner_indicator")
.child(
Button::new("player")
.variant(ButtonVariant::Ghost)
.color(Some(TextColor::Player(0))),
)
.tooltip(move |_, cx| {
cx.build_view(|cx| TextTooltip::new("Toggle following"))
}),
)
// TODO - Add project menu
.child(
div()
.id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
.tooltip(move |_, cx| {
cx.build_view(|cx| TextTooltip::new("Recent Projects"))
}),
)
// TODO - Add git menu
.child(
div()
.id("titlebar_git_menu_button")
.child(
Button::new("branch_name")
.variant(ButtonVariant::Ghost)
.color(Some(TextColor::Muted)),
)
.tooltip(move |_, cx| {
// todo!() Replace with real action.
#[gpui::action]
struct NoAction {}
cx.build_view(|cx| {
TextTooltip::new("Recent Branches")
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
"cmd-b",
NoAction {},
None,
)))
.meta("Only local branches shown")
})
}),
),
) // self.titlebar_item
.child(h_stack().child(Label::new("Right side titlebar item")))
}
fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
let active_entry = self.active_project_path(cx);
self.project
@@ -3719,7 +3646,7 @@ impl Render for Workspace {
.items_start()
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().background)
.child(self.render_titlebar(cx))
.children(self.titlebar_item.clone())
.child(
// todo! should this be a component a view?
div()
+1 -1
View File
@@ -23,7 +23,7 @@ ai = { package = "ai2", path = "../ai2"}
call = { package = "call2", path = "../call2" }
# channel = { path = "../channel" }
cli = { path = "../cli" }
# collab_ui = { path = "../collab_ui" }
collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
collections = { path = "../collections" }
command_palette = { package="command_palette2", path = "../command_palette2" }
# component_test = { path = "../component_test" }
+1 -1
View File
@@ -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);
-4
View File
@@ -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 =