1381 lines · 52.0 KB · Rust
RawDownload
use crate::{collab_panel, ChatPanelButton, ChatPanelSettings};use anyhow::Result;use call::{room, ActiveCall};use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};use client::{ChannelId, Client};use collections::HashMap;use db::kvp::KEY_VALUE_STORE;use editor::{actions, Editor};use gpui::{    actions, div, list, prelude::*, px, Action, App, AsyncWindowContext, ClipboardItem, Context,    CursorStyle, DismissEvent, ElementId, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,    HighlightStyle, ListOffset, ListScrollEvent, ListState, Render, Stateful, Subscription, Task,    WeakEntity, Window,};use language::LanguageRegistry;use menu::Confirm;use message_editor::MessageEditor;use project::Fs;use rich_text::{Highlight, RichText};use serde::{Deserialize, Serialize};use settings::Settings;use std::{sync::Arc, time::Duration};use time::{OffsetDateTime, UtcOffset};use ui::{    prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu,    Tab, TabBar, Tooltip,};use util::{ResultExt, TryFutureExt};use workspace::{    dock::{DockPosition, Panel, PanelEvent},    Workspace,}; mod message_editor; const MESSAGE_LOADING_THRESHOLD: usize = 50;const CHAT_PANEL_KEY: &str = "ChatPanel"; pub fn init(cx: &mut App) {    cx.observe_new(|workspace: &mut Workspace, _, _| {        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {            workspace.toggle_panel_focus::<ChatPanel>(window, cx);        });    })    .detach();} pub struct ChatPanel {    client: Arc<Client>,    channel_store: Entity<ChannelStore>,    languages: Arc<LanguageRegistry>,    message_list: ListState,    active_chat: Option<(Entity<ChannelChat>, Subscription)>,    message_editor: Entity<MessageEditor>,    local_timezone: UtcOffset,    fs: Arc<dyn Fs>,    width: Option<Pixels>,    active: bool,    pending_serialization: Task<Option<()>>,    subscriptions: Vec<gpui::Subscription>,    is_scrolled_to_bottom: bool,    markdown_data: HashMap<ChannelMessageId, RichText>,    focus_handle: FocusHandle,    open_context_menu: Option<(u64, Subscription)>,    highlighted_message: Option<(u64, Task<()>)>,    last_acknowledged_message_id: Option<u64>,} #[derive(Serialize, Deserialize)]struct SerializedChatPanel {    width: Option<Pixels>,} actions!(chat_panel, [ToggleFocus]); impl ChatPanel {    pub fn new(        workspace: &mut Workspace,        window: &mut Window,        cx: &mut Context<Workspace>,    ) -> Entity<Self> {        let fs = workspace.app_state().fs.clone();        let client = workspace.app_state().client.clone();        let channel_store = ChannelStore::global(cx);        let user_store = workspace.app_state().user_store.clone();        let languages = workspace.app_state().languages.clone();         let input_editor = cx.new(|cx| {            MessageEditor::new(                languages.clone(),                user_store.clone(),                None,                cx.new(|cx| Editor::auto_height(4, window, cx)),                window,                cx,            )        });         cx.new(|cx| {            let entity = cx.entity().downgrade();            let message_list = ListState::new(                0,                gpui::ListAlignment::Bottom,                px(1000.),                move |ix, window, cx| {                    if let Some(entity) = entity.upgrade() {                        entity.update(cx, |this: &mut Self, cx| {                            this.render_message(ix, window, cx).into_any_element()                        })                    } else {                        div().into_any()                    }                },            );             message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, _, cx| {                if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {                    this.load_more_messages(cx);                }                this.is_scrolled_to_bottom = !event.is_scrolled;            }));             let local_offset = chrono::Local::now().offset().local_minus_utc();            let mut this = Self {                fs,                client,                channel_store,                languages,                message_list,                active_chat: Default::default(),                pending_serialization: Task::ready(None),                message_editor: input_editor,                local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),                subscriptions: Vec::new(),                is_scrolled_to_bottom: true,                active: false,                width: None,                markdown_data: Default::default(),                focus_handle: cx.focus_handle(),                open_context_menu: None,                highlighted_message: None,                last_acknowledged_message_id: None,            };             if let Some(channel_id) = ActiveCall::global(cx)                .read(cx)                .room()                .and_then(|room| room.read(cx).channel_id())            {                this.select_channel(channel_id, None, cx)                    .detach_and_log_err(cx);            }             this.subscriptions.push(cx.subscribe(                &ActiveCall::global(cx),                move |this: &mut Self, call, event: &room::Event, cx| match event {                    room::Event::RoomJoined { channel_id } => {                        if let Some(channel_id) = channel_id {                            this.select_channel(*channel_id, None, cx)                                .detach_and_log_err(cx);                             if call                                .read(cx)                                .room()                                .is_some_and(|room| room.read(cx).contains_guests())                            {                                cx.emit(PanelEvent::Activate)                            }                        }                    }                    room::Event::RoomLeft { channel_id } => {                        if channel_id == &this.channel_id(cx) {                            cx.emit(PanelEvent::Close)                        }                    }                    _ => {}                },            ));             this        })    }     pub fn channel_id(&self, cx: &App) -> Option<ChannelId> {        self.active_chat            .as_ref()            .map(|(chat, _)| chat.read(cx).channel_id)    }     pub fn is_scrolled_to_bottom(&self) -> bool {        self.is_scrolled_to_bottom    }     pub fn active_chat(&self) -> Option<Entity<ChannelChat>> {        self.active_chat.as_ref().map(|(chat, _)| chat.clone())    }     pub fn load(        workspace: WeakEntity<Workspace>,        cx: AsyncWindowContext,    ) -> Task<Result<Entity<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_in(&mut cx, |workspace, window, cx| {                let panel = Self::new(workspace, window, cx);                if let Some(serialized_panel) = serialized_panel {                    panel.update(cx, |panel, cx| {                        panel.width = serialized_panel.width.map(|r| r.round());                        cx.notify();                    });                }                panel            })        })    }     fn serialize(&mut self, cx: &mut Context<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 set_active_chat(&mut self, chat: Entity<ChannelChat>, cx: &mut Context<Self>) {        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {            self.markdown_data.clear();            self.message_list.reset(chat.read(cx).message_count());            self.message_editor.update(cx, |editor, cx| {                editor.set_channel_chat(chat.clone(), cx);                editor.clear_reply_to_message_id();            });            let subscription = cx.subscribe(&chat, Self::channel_did_change);            self.active_chat = Some((chat, subscription));            self.acknowledge_last_message(cx);            cx.notify();        }    }     fn channel_did_change(        &mut self,        _: Entity<ChannelChat>,        event: &ChannelChatEvent,        cx: &mut Context<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::UpdateMessage {                message_id,                message_ix,            } => {                self.message_list.splice(*message_ix..*message_ix + 1, 1);                self.markdown_data.remove(message_id);            }            ChannelChatEvent::NewMessage {                channel_id,                message_id,            } => {                if !self.active {                    self.channel_store.update(cx, |store, cx| {                        store.update_latest_message_id(*channel_id, *message_id, cx)                    })                }            }        }        cx.notify();    }     fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) {        if self.active && self.is_scrolled_to_bottom {            if let Some((chat, _)) = &self.active_chat {                if let Some(channel_id) = self.channel_id(cx) {                    self.last_acknowledged_message_id = self                        .channel_store                        .read(cx)                        .last_acknowledge_message_id(channel_id);                }                 chat.update(cx, |chat, cx| {                    chat.acknowledge_last_message(cx);                });            }        }    }     fn render_replied_to_message(        &mut self,        message_id: Option<ChannelMessageId>,        reply_to_message: &Option<ChannelMessage>,        cx: &mut Context<Self>,    ) -> impl IntoElement {        let reply_to_message = match reply_to_message {            None => {                return div().child(                    h_flex()                        .text_ui_xs(cx)                        .my_0p5()                        .px_0p5()                        .gap_x_1()                        .rounded_md()                        .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))                        .when(reply_to_message.is_none(), |el| {                            el.child(                                Label::new("Message has been deleted...")                                    .size(LabelSize::XSmall)                                    .color(Color::Muted),                            )                        }),                )            }            Some(val) => val,        };         let user_being_replied_to = reply_to_message.sender.clone();        let message_being_replied_to = reply_to_message.clone();         let message_element_id: ElementId = match message_id {            Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message-container", id).into(),            Some(ChannelMessageId::Pending(id)) => {                ("reply-to-pending-message-container", id).into()            } // This should never happen            None => ("composing-reply-container").into(),        };         let current_channel_id = self.channel_id(cx);        let reply_to_message_id = reply_to_message.id;         div().child(            h_flex()                .id(message_element_id)                .text_ui_xs(cx)                .my_0p5()                .px_0p5()                .gap_x_1()                .rounded_md()                .overflow_hidden()                .hover(|style| style.bg(cx.theme().colors().element_background))                .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))                .child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7)))                .child(                    Label::new(format!("@{}", user_being_replied_to.github_login))                        .size(LabelSize::XSmall)                        .weight(FontWeight::SEMIBOLD)                        .color(Color::Muted),                )                .child(                    div().overflow_y_hidden().child(                        Label::new(message_being_replied_to.body.replace('\n', " "))                            .size(LabelSize::XSmall)                            .color(Color::Default),                    ),                )                .cursor(CursorStyle::PointingHand)                .tooltip(Tooltip::text("Go to message"))                .on_click(cx.listener(move |chat_panel, _, _, cx| {                    if let Some(channel_id) = current_channel_id {                        chat_panel                            .select_channel(channel_id, reply_to_message_id.into(), cx)                            .detach_and_log_err(cx)                    }                })),        )    }     fn render_message(        &mut self,        ix: usize,        window: &mut Window,        cx: &mut Context<Self>,    ) -> impl IntoElement {        let active_chat = &self.active_chat.as_ref().unwrap().0;        let (message, is_continuation_from_previous, is_admin) =            active_chat.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 duration_since_last_message = this_message.timestamp - last_message.timestamp;                let is_continuation_from_previous = last_message.sender.id                    == this_message.sender.id                    && last_message.id != this_message.id                    && duration_since_last_message < Duration::from_secs(5 * 60);                 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_from_previous, is_admin)            });         let _is_pending = message.is_pending();         let belongs_to_user = Some(message.sender.id) == self.client.user_id();        let can_delete_message = belongs_to_user || is_admin;        let can_edit_message = belongs_to_user;         let element_id: ElementId = match message.id {            ChannelMessageId::Saved(id) => ("saved-message", id).into(),            ChannelMessageId::Pending(id) => ("pending-message", id).into(),        };         let mentioning_you = message            .mentions            .iter()            .any(|m| Some(m.1) == self.client.user_id());         let message_id = match message.id {            ChannelMessageId::Saved(id) => Some(id),            ChannelMessageId::Pending(_) => None,        };         let reply_to_message = message            .reply_to_message_id            .and_then(|id| active_chat.read(cx).find_loaded_message(id))            .cloned();         let replied_to_you =            reply_to_message.as_ref().map(|m| m.sender.id) == self.client.user_id();         let is_highlighted_message = self            .highlighted_message            .as_ref()            .is_some_and(|(id, _)| Some(id) == message_id.as_ref());        let background = if is_highlighted_message {            cx.theme().status().info_background        } else if mentioning_you || replied_to_you {            cx.theme().colors().background        } else {            cx.theme().colors().panel_background        };         let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id();         v_flex()            .w_full()            .relative()            .group("")            .when(!is_continuation_from_previous, |this| this.pt_2())            .child(                div()                    .group("")                    .bg(background)                    .rounded_md()                    .overflow_hidden()                    .px_1p5()                    .py_0p5()                    .when_some(reply_to_message_id, |el, reply_id| {                        el.when_some(message_id, |el, message_id| {                            el.when(reply_id == message_id, |el| {                                el.bg(cx.theme().colors().element_selected)                            })                        })                    })                    .when(!self.has_open_menu(message_id), |this| {                        this.hover(|style| style.bg(cx.theme().colors().element_hover))                    })                    .when(message.reply_to_message_id.is_some(), |el| {                        el.child(self.render_replied_to_message(                            Some(message.id),                            &reply_to_message,                            cx,                        ))                        .when(is_continuation_from_previous, |this| this.mt_2())                    })

Lines 1–500 of 1381.