add inbox view
This commit is contained in:
@@ -0,0 +1,610 @@
|
||||
use assets::CustomIconName;
|
||||
use dock::{BasePanel, DockArea, Panel, PanelEvent};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
|
||||
Subscription, WeakEntity, Window, div, px,
|
||||
};
|
||||
use gpui_component::input::{Input, InputState};
|
||||
use gpui_component::scroll::ScrollableElement;
|
||||
use gpui_component::{ActiveTheme, Icon, IconName, IconNamed, Sizable, StyledExt, h_flex, v_flex};
|
||||
use nostr::prelude::{Event, Kind};
|
||||
use signed_core::{Announcement, COVER_NOTE_KIND, InboxItem, RepoAddr, activity_subject};
|
||||
use signed_state::{Backend, BackendEvent, InboxStore, ProfileStore, RepoListStore};
|
||||
use signed_ui::{CountBadge, SegmentButton, UserAvatar};
|
||||
use utils::relative_time;
|
||||
|
||||
use super::open_repo_panel;
|
||||
use super::sidebar::create_repo_dialog;
|
||||
|
||||
/// Notification groups shown before the `Show all` toggle is used.
|
||||
const NOTIFICATION_PREVIEW: usize = 5;
|
||||
|
||||
/// Activity rows shown in `Continue where you left off`.
|
||||
const ACTIVITY_SHOWN: usize = 15;
|
||||
|
||||
pub struct InboxView {
|
||||
focus_handle: FocusHandle,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
/// Derives the notification and activity lists while the panel is open.
|
||||
store: Entity<InboxStore>,
|
||||
/// Search box filtering the `My repositories` column.
|
||||
search: Entity<InputState>,
|
||||
/// Whether the inbox list is expanded past [`NOTIFICATION_PREVIEW`].
|
||||
show_all: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl InboxView {
|
||||
pub fn new(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let backend = Backend::global(cx);
|
||||
let inbox = backend.read(cx).inbox();
|
||||
|
||||
let store = cx.new(InboxStore::new);
|
||||
let search = cx.new(|cx| InputState::new(window, cx).placeholder("Search..."));
|
||||
|
||||
// Drive the store from the global inbox state and from backend events.
|
||||
let _subscriptions = vec![
|
||||
cx.observe(&inbox, |this, _inbox, cx| {
|
||||
this.store.update(cx, |store, cx| store.sync_state(cx));
|
||||
}),
|
||||
cx.subscribe(&backend, |this, _backend, event, cx| {
|
||||
if matches!(
|
||||
event,
|
||||
BackendEvent::SignerChanged | BackendEvent::SignerRequired
|
||||
) {
|
||||
this.show_all = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
this.store
|
||||
.update(cx, |store, cx| store.handle_backend_event(event, cx));
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
store,
|
||||
search,
|
||||
show_all: false,
|
||||
_subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark every known notification read.
|
||||
fn mark_all_read(&mut self, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| store.mark_all_read(cx));
|
||||
}
|
||||
|
||||
/// Open a repository's detail panel.
|
||||
fn open_repo(
|
||||
&mut self,
|
||||
announcement: &Announcement,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
open_repo_panel(&self.dock_area, announcement, window, &mut *cx);
|
||||
}
|
||||
|
||||
/// Show the existing Create Repository dialog.
|
||||
fn open_create_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
create_repo_dialog::open(self.dock_area.clone(), window, cx);
|
||||
}
|
||||
|
||||
/// Display name of the repository at `addr`, from the announcement store.
|
||||
fn repo_name(&self, addr: Option<&RepoAddr>, cx: &App) -> Option<SharedString> {
|
||||
let addr = addr?;
|
||||
RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements
|
||||
.iter()
|
||||
.find(|announcement| announcement.addr() == *addr)
|
||||
.map(display_name)
|
||||
}
|
||||
|
||||
/// Bordered card with a header bar and a body.
|
||||
fn section(&self, header: impl IntoElement, body: impl IntoElement, cx: &App) -> AnyElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.rounded(cx.theme().radius)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.bg(cx.theme().muted.opacity(0.5))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(header),
|
||||
)
|
||||
.child(body)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_inbox_panel(
|
||||
&self,
|
||||
unread: usize,
|
||||
visible: &[&InboxItem],
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let shown = if self.show_all {
|
||||
visible.len()
|
||||
} else {
|
||||
visible.len().min(NOTIFICATION_PREVIEW)
|
||||
};
|
||||
|
||||
let mut body = v_flex().w_full();
|
||||
|
||||
if visible.is_empty() {
|
||||
body = body.child(empty_state(IconName::Inbox, "You're all caught up.", cx));
|
||||
} else {
|
||||
for (ix, item) in visible.iter().take(shown).enumerate() {
|
||||
body = body.child(self.render_notification_row(ix, item, cx));
|
||||
}
|
||||
|
||||
if visible.len() > NOTIFICATION_PREVIEW {
|
||||
let label = if self.show_all {
|
||||
"Show less"
|
||||
} else {
|
||||
"Show all"
|
||||
};
|
||||
body = body.child(div().px_3().py_2().child(
|
||||
SegmentButton::new("show-all", label).on_click(cx.listener(
|
||||
|this, _event, _window, cx| {
|
||||
this.show_all = !this.show_all;
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let header = h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Inbox).small())
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::from("Inbox")),
|
||||
)
|
||||
.when(unread > 0, |this| this.child(CountBadge::new(unread)))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
SegmentButton::new("mark-all-read", "Mark all read")
|
||||
.on_click(cx.listener(|this, _event, _window, cx| this.mark_all_read(cx))),
|
||||
);
|
||||
|
||||
self.section(header, body, cx)
|
||||
}
|
||||
|
||||
fn render_notification_row(
|
||||
&self,
|
||||
ix: usize,
|
||||
item: &InboxItem,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let Some(newest) = item.events.first() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let profiles = ProfileStore::global(cx);
|
||||
let profile = profiles.read(cx).get(&newest.pubkey);
|
||||
let kind = item.root_kind.unwrap_or(newest.kind);
|
||||
let subject = SharedString::from(activity_subject(newest));
|
||||
let repo = self.repo_name(item.address.as_ref(), cx);
|
||||
let age = relative_time(item.latest_activity());
|
||||
let unread = item.is_unread();
|
||||
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.items_center()
|
||||
.rounded(cx.theme().radius)
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.child(UserAvatar::new(profile.name()).picture(profile.picture()))
|
||||
.child(div().flex_shrink_0().child(kind_icon(kind)))
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.when(unread, |this| this.font_semibold())
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.child(subject),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(kind_label(kind)))
|
||||
.when_some(repo, |this, repo| {
|
||||
this.child(SharedString::from("on")).child(repo)
|
||||
})
|
||||
.child(SharedString::from("·"))
|
||||
.child(SharedString::from(age)),
|
||||
),
|
||||
)
|
||||
.when(unread, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size(px(8.))
|
||||
.rounded(px(4.))
|
||||
.bg(cx.theme().primary),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_activity_panel(&self, activity: &[Event], cx: &mut Context<Self>) -> AnyElement {
|
||||
let mut body = v_flex().w_full();
|
||||
|
||||
if activity.is_empty() {
|
||||
body = body.child(empty_state(
|
||||
CustomIconName::Recent,
|
||||
"No recent activity.",
|
||||
cx,
|
||||
));
|
||||
} else {
|
||||
for (ix, event) in activity.iter().take(ACTIVITY_SHOWN).enumerate() {
|
||||
body = body.child(self.render_activity_row(ix, event, cx));
|
||||
}
|
||||
}
|
||||
|
||||
let header = h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(Icon::new(CustomIconName::Recent).small())
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::from("Continue where you left off")),
|
||||
);
|
||||
|
||||
self.section(header, body, cx)
|
||||
}
|
||||
|
||||
fn render_activity_row(&self, ix: usize, event: &Event, cx: &mut Context<Self>) -> AnyElement {
|
||||
let kind = event.kind;
|
||||
let subject = SharedString::from(activity_subject(event));
|
||||
let repo = self.repo_name(event.tags.coordinates().next().as_ref(), cx);
|
||||
let age = relative_time(event.created_at);
|
||||
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.items_center()
|
||||
.rounded(cx.theme().radius)
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.child(div().flex_shrink_0().child(kind_icon(kind)))
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.child(subject),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(kind_label(kind)))
|
||||
.when_some(repo, |this, repo| {
|
||||
this.child(SharedString::from("on")).child(repo)
|
||||
})
|
||||
.child(SharedString::from("·"))
|
||||
.child(SharedString::from(age)),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_repos_panel(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(me) = Backend::global(cx).read(cx).current_user() else {
|
||||
let header = h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(Icon::new(CustomIconName::GitBranch).small())
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::from("My repositories")),
|
||||
);
|
||||
let body = v_flex().w_full().child(empty_state(
|
||||
CustomIconName::GitBranch,
|
||||
"Sign in to see your repositories.",
|
||||
cx,
|
||||
));
|
||||
return self.section(header, body, cx);
|
||||
};
|
||||
|
||||
let query = self.search.read(cx).value().trim().to_lowercase();
|
||||
let repos: Vec<Announcement> = RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements_of(&me)
|
||||
.into_iter()
|
||||
.filter(|announcement| {
|
||||
query.is_empty()
|
||||
|| display_name(announcement).to_lowercase().contains(&query)
|
||||
|| announcement.id.to_lowercase().contains(&query)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut body = v_flex().w_full().child(
|
||||
div().px_3().py_2().child(
|
||||
Input::new(&self.search)
|
||||
.cleanable(true)
|
||||
.w_full()
|
||||
.text_sm()
|
||||
.border_color(cx.theme().muted)
|
||||
.bg(cx.theme().muted)
|
||||
.prefix(Icon::new(IconName::Search).small()),
|
||||
),
|
||||
);
|
||||
|
||||
if repos.is_empty() {
|
||||
body = body.child(empty_state(
|
||||
CustomIconName::GitBranch,
|
||||
"No repositories yet.",
|
||||
cx,
|
||||
));
|
||||
} else {
|
||||
for (ix, announcement) in repos.iter().enumerate() {
|
||||
body = body.child(self.render_repo_row(ix, announcement, cx));
|
||||
}
|
||||
}
|
||||
|
||||
let header = h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(Icon::new(CustomIconName::GitBranch).small())
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.child(SharedString::from("My repositories")),
|
||||
)
|
||||
.when(!repos.is_empty(), |this| {
|
||||
this.child(CountBadge::new(repos.len()))
|
||||
})
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
SegmentButton::new("new-repo", "New")
|
||||
.icon(Icon::new(CustomIconName::CirclePlus))
|
||||
.primary()
|
||||
.on_click(
|
||||
cx.listener(|this, _event, window, cx| this.open_create_repo(window, cx)),
|
||||
),
|
||||
);
|
||||
|
||||
self.section(header, body, cx)
|
||||
}
|
||||
|
||||
fn render_repo_row(
|
||||
&self,
|
||||
ix: usize,
|
||||
announcement: &Announcement,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let name = display_name(announcement);
|
||||
let description = announcement.description.clone().unwrap_or_default();
|
||||
let activity = RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.last_activity
|
||||
.get(&announcement.addr())
|
||||
.copied();
|
||||
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.items_center()
|
||||
.rounded(cx.theme().radius)
|
||||
.hover(|this| this.bg(cx.theme().list_hover))
|
||||
.child(
|
||||
Icon::new(CustomIconName::GitBranch)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.child(name),
|
||||
)
|
||||
.when(!description.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.child(SharedString::from(description)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(activity, |this, activity| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(relative_time(activity))),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let announcement = announcement.clone();
|
||||
move |this, _event, window, cx| this.open_repo(&announcement, window, cx)
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// Name to show for a repository, its `name` tag or its id.
|
||||
fn display_name(announcement: &Announcement) -> SharedString {
|
||||
announcement
|
||||
.name
|
||||
.as_deref()
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||
}
|
||||
|
||||
/// Leading icon for a notification or activity kind.
|
||||
fn kind_icon(kind: Kind) -> Icon {
|
||||
if kind == COVER_NOTE_KIND {
|
||||
return Icon::new(IconName::FileText).small();
|
||||
}
|
||||
|
||||
match kind {
|
||||
Kind::GitIssue => Icon::new(CustomIconName::GitIssueOpen),
|
||||
Kind::GitPullRequest | Kind::GitPullRequestUpdate => {
|
||||
Icon::new(CustomIconName::GitPullRequest)
|
||||
}
|
||||
Kind::GitPatch => Icon::new(CustomIconName::GitCommit),
|
||||
Kind::Comment => Icon::new(IconName::FileText),
|
||||
Kind::GitStatusOpen
|
||||
| Kind::GitStatusApplied
|
||||
| Kind::GitStatusClosed
|
||||
| Kind::GitStatusDraft => Icon::new(IconName::CircleCheck),
|
||||
_ => Icon::new(IconName::Bell),
|
||||
}
|
||||
.small()
|
||||
}
|
||||
|
||||
/// Short noun for a notification or activity kind.
|
||||
fn kind_label(kind: Kind) -> &'static str {
|
||||
if kind == COVER_NOTE_KIND {
|
||||
return "note";
|
||||
}
|
||||
|
||||
match kind {
|
||||
Kind::GitIssue => "issue",
|
||||
Kind::GitPullRequest => "PR",
|
||||
Kind::GitPullRequestUpdate => "PR update",
|
||||
Kind::GitPatch => "patch",
|
||||
Kind::Comment => "comment",
|
||||
Kind::GitStatusOpen
|
||||
| Kind::GitStatusApplied
|
||||
| Kind::GitStatusClosed
|
||||
| Kind::GitStatusDraft => "status",
|
||||
_ => "activity",
|
||||
}
|
||||
}
|
||||
|
||||
/// Centered muted icon and message filling its container.
|
||||
fn empty_state(icon: impl IconNamed, message: &str, cx: &App) -> AnyElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.py_8()
|
||||
.child(
|
||||
Icon::new(icon)
|
||||
.large()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(message)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
impl BasePanel for InboxView {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"inbox"
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for InboxView {
|
||||
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().text_sm().child(SharedString::from("Inbox"))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for InboxView {}
|
||||
|
||||
impl Focusable for InboxView {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InboxView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (unread, notifications, activity) = {
|
||||
let store = self.store.read(cx);
|
||||
(
|
||||
store.unread_count,
|
||||
store.notifications.clone(),
|
||||
store.activity.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let visible: Vec<&InboxItem> = notifications.iter().filter(|item| !item.archived).collect();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.image_cache(gpui::retain_all("inbox"))
|
||||
.child(
|
||||
v_flex().size_full().overflow_y_scrollbar().child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_4()
|
||||
.p_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.gap_4()
|
||||
.child(self.render_inbox_panel(unread, &visible, cx))
|
||||
.child(self.render_activity_panel(&activity, cx)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w(px(300.))
|
||||
.flex_shrink_0()
|
||||
.child(self.render_repos_panel(cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
mod dialog_state;
|
||||
mod inbox;
|
||||
mod repo_detail;
|
||||
mod repo_list;
|
||||
pub(crate) mod sidebar;
|
||||
|
||||
pub use inbox::InboxView;
|
||||
pub use repo_detail::RepoDetailView;
|
||||
pub(crate) use repo_detail::open_repo_panel;
|
||||
pub use repo_list::RepoListView;
|
||||
|
||||
@@ -21,11 +21,11 @@ use signed_core::{Announcement, RepoAddr, identifier_from_name};
|
||||
use signed_state::{
|
||||
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Profile, ProfileStore, RepoListStore,
|
||||
};
|
||||
use signed_ui::{NavItem, PixelAvatar, UserAvatar, title_bar_drag_handlers};
|
||||
use signed_ui::{CountBadge, NavItem, PixelAvatar, UserAvatar, title_bar_drag_handlers};
|
||||
|
||||
use super::{RepoDetailView, RepoListView, open_repo_panel};
|
||||
use super::{InboxView, RepoDetailView, RepoListView, open_repo_panel};
|
||||
|
||||
mod create_repo_dialog;
|
||||
pub(crate) mod create_repo_dialog;
|
||||
pub(crate) mod grasp_servers;
|
||||
mod import_dialog;
|
||||
mod onboarding_dialog;
|
||||
@@ -37,7 +37,10 @@ use self::onboarding_dialog::OnboardingState;
|
||||
pub struct SidebarPanel {
|
||||
focus_handle: FocusHandle,
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
inbox: Option<WeakEntity<InboxView>>,
|
||||
explore: Option<WeakEntity<RepoListView>>,
|
||||
/// Unread notification groups, shown as the inbox nav item's badge.
|
||||
unread: usize,
|
||||
/// Artwork for the sign-in screen.
|
||||
banner: SharedString,
|
||||
/// The signed-in user's announced repositories, newest first.
|
||||
@@ -99,10 +102,22 @@ impl SidebarPanel {
|
||||
}
|
||||
}));
|
||||
|
||||
// The inbox nav item shows the unread notification count as a badge.
|
||||
let inbox = backend.read(cx).inbox();
|
||||
subscriptions.push(cx.observe(&inbox, |this, inbox, cx| {
|
||||
let unread = inbox.read(cx).unread_count;
|
||||
if this.unread != unread {
|
||||
this.unread = unread;
|
||||
cx.notify();
|
||||
}
|
||||
}));
|
||||
|
||||
let mut this = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
inbox: None,
|
||||
explore: None,
|
||||
unread: inbox.read(cx).unread_count,
|
||||
banner: pick_banner(),
|
||||
announcements: Arc::new(Vec::new()),
|
||||
local_repos: Arc::new(Vec::new()),
|
||||
@@ -203,6 +218,20 @@ impl SidebarPanel {
|
||||
});
|
||||
}
|
||||
|
||||
/// Open the inbox home panel in the dock area's center.
|
||||
pub fn open_inbox(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.inbox.as_ref().and_then(WeakEntity::upgrade).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let panel = cx.new(|cx| InboxView::new(self.dock_area.clone(), window, cx));
|
||||
self.inbox = Some(panel.downgrade());
|
||||
|
||||
let _ = self.dock_area.update(cx, |dock_area, cx| {
|
||||
add_center_panel(dock_area, panel_handle(panel), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Open the Explore repository list panel in the dock area's center.
|
||||
pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self
|
||||
@@ -615,8 +644,11 @@ impl Render for SidebarPanel {
|
||||
.justify_start()
|
||||
.child(
|
||||
NavItem::new("inbox", "Inbox", Icon::new(IconName::Inbox).small())
|
||||
.when(self.unread > 0, |this| {
|
||||
this.suffix(CountBadge::new(self.unread))
|
||||
})
|
||||
.on_click(cx.listener(|this, _ev, window, cx| {
|
||||
this.open_explore(window, cx)
|
||||
this.open_inbox(window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
|
||||
Reference in New Issue
Block a user