823 lines · 30.2 KB · Rust
RawDownload
use crate::{chat_panel::ChatPanel, NotificationPanelSettings};use anyhow::Result;use channel::ChannelStore;use client::{ChannelId, Client, Notification, User, UserStore};use collections::HashMap;use db::kvp::KEY_VALUE_STORE;use futures::StreamExt;use gpui::{    actions, div, img, list, px, AnyElement, App, AsyncWindowContext, Context, CursorStyle,    DismissEvent, Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,    IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render,    StatefulInteractiveElement, Styled, Task, WeakEntity, Window,};use notifications::{NotificationEntry, NotificationEvent, NotificationStore};use project::Fs;use rpc::proto;use serde::{Deserialize, Serialize};use settings::{Settings, SettingsStore};use std::{sync::Arc, time::Duration};use time::{OffsetDateTime, UtcOffset};use ui::{    h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip,};use util::{ResultExt, TryFutureExt};use workspace::notifications::NotificationId;use workspace::{    dock::{DockPosition, Panel, PanelEvent},    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: &str = "NotificationPanel"; pub struct NotificationPanel {    client: Arc<Client>,    user_store: Entity<UserStore>,    channel_store: Entity<ChannelStore>,    notification_store: Entity<NotificationStore>,    fs: Arc<dyn Fs>,    width: Option<Pixels>,    active: bool,    notification_list: ListState,    pending_serialization: Task<Option<()>>,    subscriptions: Vec<gpui::Subscription>,    workspace: WeakEntity<Workspace>,    current_notification_toast: Option<(u64, Task<()>)>,    local_timezone: UtcOffset,    focus_handle: FocusHandle,    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,    unseen_notifications: Vec<NotificationEntry>,} #[derive(Serialize, Deserialize)]struct SerializedNotificationPanel {    width: Option<Pixels>,} #[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 App) {    cx.observe_new(|workspace: &mut Workspace, _, _| {        workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {            workspace.toggle_panel_focus::<NotificationPanel>(window, cx);        });    })    .detach();} impl NotificationPanel {    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 user_store = workspace.app_state().user_store.clone();        let workspace_handle = workspace.weak_handle();         cx.new(|cx| {            let mut status = client.status();            cx.spawn_in(window, |this, mut cx| async move {                while (status.next().await).is_some() {                    if this                        .update(&mut cx, |_: &mut Self, cx| {                            cx.notify();                        })                        .is_err()                    {                        break;                    }                }            })            .detach();             let entity = cx.entity().downgrade();            let notification_list =                ListState::new(0, ListAlignment::Top, px(1000.), move |ix, window, cx| {                    entity                        .upgrade()                        .and_then(|entity| {                            entity.update(cx, |this, cx| this.render_notification(ix, window, cx))                        })                        .unwrap_or_else(|| div().into_any())                });            notification_list.set_scroll_handler(cx.listener(                |this, event: &ListScrollEvent, _, cx| {                    if event.count.saturating_sub(event.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 local_offset = chrono::Local::now().offset().local_minus_utc();            let mut this = Self {                fs,                client,                user_store,                local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),                channel_store: ChannelStore::global(cx),                notification_store: NotificationStore::global(cx),                notification_list,                pending_serialization: Task::ready(None),                workspace: workspace_handle,                focus_handle: cx.focus_handle(),                current_notification_toast: None,                subscriptions: Vec::new(),                active: false,                mark_as_read_tasks: HashMap::default(),                width: None,                unseen_notifications: Vec::new(),            };             let mut old_dock_position = this.position(window, cx);            this.subscriptions.extend([                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),                cx.subscribe_in(                    &this.notification_store,                    window,                    Self::on_notification_event,                ),                cx.observe_global_in::<SettingsStore>(                    window,                    move |this: &mut Self, window, cx| {                        let new_dock_position = this.position(window, 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: 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(NOTIFICATION_PANEL_KEY) })                .await                .log_err()                .flatten()            {                Some(serde_json::from_str::<SerializedNotificationPanel>(&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(|w| w.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(                        NOTIFICATION_PANEL_KEY.into(),                        serde_json::to_string(&SerializedNotificationPanel { width })?,                    )                    .await?;                anyhow::Ok(())            }            .log_err(),        );    }     fn render_notification(        &mut self,        ix: usize,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Option<AnyElement> {        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 response = entry.response;        let notification = entry.notification.clone();         if self.active && !entry.is_read {            self.did_render_notification(notification_id, &notification, window, cx);        }         let relative_timestamp = time_format::format_localized_timestamp(            timestamp,            now,            self.local_timezone,            time_format::TimestampFormat::Relative,        );         let absolute_timestamp = time_format::format_localized_timestamp(            timestamp,            now,            self.local_timezone,            time_format::TimestampFormat::Absolute,        );         Some(            div()                .id(ix)                .flex()                .flex_row()                .size_full()                .px_2()                .py_1()                .gap_2()                .hover(|style| style.bg(cx.theme().colors().element_hover))                .when(can_navigate, |el| {                    el.cursor(CursorStyle::PointingHand).on_click({                        let notification = notification.clone();                        cx.listener(move |this, _, window, cx| {                            this.did_click_notification(&notification, window, cx)                        })                    })                })                .children(actor.map(|actor| {                    img(actor.avatar_uri.clone())                        .flex_none()                        .w_8()                        .h_8()                        .rounded_full()                }))                .child(                    v_flex()                        .gap_1()                        .size_full()                        .overflow_hidden()                        .child(Label::new(text.clone()))                        .child(                            h_flex()                                .child(                                    div()                                        .id("notification_timestamp")                                        .hover(|style| {                                            style                                                .bg(cx.theme().colors().element_selected)                                                .rounded_md()                                        })                                        .child(Label::new(relative_timestamp).color(Color::Muted))                                        .tooltip(move |_, cx| {                                            Tooltip::simple(absolute_timestamp.clone(), cx)                                        }),                                )                                .children(if let Some(is_accepted) = response {                                    Some(div().flex().flex_grow().justify_end().child(Label::new(                                        if is_accepted {                                            "You accepted"                                        } else {                                            "You declined"                                        },                                    )))                                } else if needs_response {                                    Some(                                        h_flex()                                            .flex_grow()                                            .justify_end()                                            .child(Button::new("decline", "Decline").on_click({                                                let notification = notification.clone();                                                let entity = cx.entity().clone();                                                move |_, _, cx| {                                                    entity.update(cx, |this, cx| {                                                        this.respond_to_notification(                                                            notification.clone(),                                                            false,                                                            cx,                                                        )                                                    });                                                }                                            }))                                            .child(Button::new("accept", "Accept").on_click({                                                let notification = notification.clone();                                                let entity = cx.entity().clone();                                                move |_, _, cx| {                                                    entity.update(cx, |this, cx| {                                                        this.respond_to_notification(                                                            notification.clone(),                                                            true,                                                            cx,                                                        )                                                    });                                                }                                            })),                                    )                                } else {                                    None                                }),                        ),                )                .into_any(),        )    }     fn present_notification(        &self,        entry: &NotificationEntry,        cx: &App,    ) -> 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(ChannelId(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(ChannelId(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,        window: &mut Window,        cx: &mut Context<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_in(window, |this, mut cx| async move {                        cx.background_executor().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,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if let Notification::ChannelMessageMention {            message_id,            channel_id,            ..        } = notification.clone()        {            if let Some(workspace) = self.workspace.upgrade() {                window.defer(cx, move |window, cx| {                    workspace.update(cx, |workspace, cx| {                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {                            panel.update(cx, |panel, cx| {                                panel                                    .select_channel(ChannelId(channel_id), Some(message_id), cx)                                    .detach_and_log_err(cx);                            });                        }                    });                });            }        }    }     fn is_showing_notification(&self, notification: &Notification, cx: &mut Context<Self>) -> bool {        if !self.active {            return false;        }         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {            if let Some(workspace) = self.workspace.upgrade() {                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {                    let panel = panel.read(cx);                    panel.is_scrolled_to_bottom()                        && panel

Lines 1–500 of 823.