update
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run
This commit is contained in:
+111
-141
@@ -3,7 +3,6 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use assets::CustomIconName;
|
||||
use dock::{BasePanel, DockArea, Panel, PanelEvent};
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
@@ -20,7 +19,7 @@ use signed_state::{
|
||||
use signed_ui::{CountBadge, UserAvatar};
|
||||
use utils::relative_time;
|
||||
|
||||
use super::{RepoItem, open_repo_item, open_repo_panel};
|
||||
use super::{RepoItem, open_repo_item};
|
||||
|
||||
/// Delay between a refresh request and the actual re-query.
|
||||
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
|
||||
@@ -124,8 +123,16 @@ impl InboxView {
|
||||
let Some(me) = Backend::global(cx).read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
let all = self.all_notification_events();
|
||||
let inbox = Backend::global(cx).read(cx).inbox();
|
||||
|
||||
let all: Vec<Event> = self
|
||||
.threads
|
||||
.iter()
|
||||
.flat_map(|item| item.events.iter().cloned())
|
||||
.collect();
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
let inbox = backend.read(cx).inbox();
|
||||
|
||||
inbox.update(cx, |inbox, cx| inbox.mark_all_read(&all, me, cx));
|
||||
}
|
||||
|
||||
@@ -274,7 +281,7 @@ impl InboxView {
|
||||
fn rebuild(&mut self, cx: &mut Context<Self>) {
|
||||
let backend = Backend::global(cx);
|
||||
let repo_list = RepoListStore::global(cx);
|
||||
let mut sections = group_sections(&self.threads);
|
||||
let mut sections = self.group_sections();
|
||||
|
||||
if let Some(me) = backend.read(cx).current_user() {
|
||||
for announcement in repo_list.read(cx).announcements_of(&me) {
|
||||
@@ -296,11 +303,72 @@ impl InboxView {
|
||||
|
||||
sections.sort_by_key(|section| std::cmp::Reverse(section.latest));
|
||||
|
||||
let rows = flatten_rows(§ions);
|
||||
let rows = self.flatten_rows(§ions);
|
||||
self.sections = Arc::new(sections);
|
||||
self.rows = Arc::new(rows);
|
||||
}
|
||||
|
||||
/// Group the threads into one section per repository.
|
||||
fn group_sections(&self) -> Vec<InboxSection> {
|
||||
let mut by_repo: HashMap<Option<RepoAddr>, InboxSection> = HashMap::new();
|
||||
|
||||
for (ix, item) in self.threads.iter().enumerate() {
|
||||
if item.archived {
|
||||
continue;
|
||||
}
|
||||
|
||||
let address = item.address.clone();
|
||||
let section = by_repo
|
||||
.entry(address.clone())
|
||||
.or_insert_with(move || InboxSection {
|
||||
address,
|
||||
unread: 0,
|
||||
entries: Vec::new(),
|
||||
latest: Timestamp::default(),
|
||||
});
|
||||
|
||||
if item.is_unread() {
|
||||
section.unread += 1;
|
||||
}
|
||||
|
||||
section.latest = section.latest.max(item.latest_activity());
|
||||
section.entries.push(ix);
|
||||
}
|
||||
|
||||
let mut sections: Vec<InboxSection> = by_repo.into_values().collect();
|
||||
|
||||
for section in &mut sections {
|
||||
section.entries.sort_by(|a, b| {
|
||||
self.threads[*b]
|
||||
.latest_activity()
|
||||
.cmp(&self.threads[*a].latest_activity())
|
||||
});
|
||||
}
|
||||
|
||||
sections.sort_by_key(|section| std::cmp::Reverse(section.latest));
|
||||
sections
|
||||
}
|
||||
|
||||
/// Flatten the sections into the list of repository headers and their rows.
|
||||
fn flatten_rows(&self, sections: &[InboxSection]) -> Vec<InboxRow> {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for (section_ix, section) in sections.iter().enumerate() {
|
||||
rows.push(InboxRow::Repo(section_ix));
|
||||
|
||||
if section.entries.is_empty() {
|
||||
rows.push(InboxRow::Empty);
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.extend(
|
||||
(0..section.entries.len()).map(|entry_ix| InboxRow::Entry(section_ix, entry_ix)),
|
||||
);
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Forget everything derived for the current user.
|
||||
fn clear(&mut self) {
|
||||
self.threads = Arc::new(Vec::new());
|
||||
@@ -313,15 +381,39 @@ impl InboxView {
|
||||
self.refresh = RefreshGate::default();
|
||||
}
|
||||
|
||||
/// Every notification event in every thread, archived threads included.
|
||||
fn all_notification_events(&self) -> Vec<Event> {
|
||||
self.threads
|
||||
fn open(
|
||||
&self,
|
||||
root: EventId,
|
||||
kind: Option<Kind>,
|
||||
address: Option<RepoAddr>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(address) = address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(announcement) = RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements
|
||||
.iter()
|
||||
.flat_map(|item| item.events.iter().cloned())
|
||||
.collect()
|
||||
.find(|announcement| announcement.addr() == address)
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let item = match kind {
|
||||
Some(Kind::GitIssue) => RepoItem::Issue(root),
|
||||
Some(Kind::GitPullRequest) => RepoItem::PullRequest(root),
|
||||
Some(Kind::GitPatch) => RepoItem::Patch,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
open_repo_item(&self.dock_area, &announcement, item, window, cx);
|
||||
}
|
||||
|
||||
fn render_entry(&self, ix: usize, cx: &App) -> AnyElement {
|
||||
fn render_entry(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
let Some(row) = self.rows.get(ix) else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
@@ -350,81 +442,19 @@ impl InboxView {
|
||||
let root = item.root;
|
||||
let kind = item.root_kind;
|
||||
let address = section.address.clone();
|
||||
let dock_area = self.dock_area.clone();
|
||||
let first = entry_ix == 0;
|
||||
let last = entry_ix + 1 == section.entries.len();
|
||||
|
||||
thread("inbox-row", ix, item, first, last, cx)
|
||||
.on_click(move |_, window, cx| {
|
||||
open_item(&dock_area, root, kind, address.clone(), window, cx);
|
||||
})
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.open(root, kind, address.clone(), window, cx)
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Group the threads into one section per repository.
|
||||
fn group_sections(threads: &[InboxItem]) -> Vec<InboxSection> {
|
||||
let mut by_repo: HashMap<Option<RepoAddr>, InboxSection> = HashMap::new();
|
||||
|
||||
for (ix, item) in threads.iter().enumerate() {
|
||||
if item.archived {
|
||||
continue;
|
||||
}
|
||||
|
||||
let address = item.address.clone();
|
||||
let section = by_repo
|
||||
.entry(address.clone())
|
||||
.or_insert_with(move || InboxSection {
|
||||
address,
|
||||
unread: 0,
|
||||
entries: Vec::new(),
|
||||
latest: Timestamp::default(),
|
||||
});
|
||||
|
||||
if item.is_unread() {
|
||||
section.unread += 1;
|
||||
}
|
||||
|
||||
section.latest = section.latest.max(item.latest_activity());
|
||||
section.entries.push(ix);
|
||||
}
|
||||
|
||||
let mut sections: Vec<InboxSection> = by_repo.into_values().collect();
|
||||
|
||||
for section in &mut sections {
|
||||
section.entries.sort_by(|a, b| {
|
||||
threads[*b]
|
||||
.latest_activity()
|
||||
.cmp(&threads[*a].latest_activity())
|
||||
});
|
||||
}
|
||||
|
||||
sections.sort_by_key(|section| std::cmp::Reverse(section.latest));
|
||||
sections
|
||||
}
|
||||
|
||||
/// Flatten the sections into the list of repository headers and their rows.
|
||||
fn flatten_rows(sections: &[InboxSection]) -> Vec<InboxRow> {
|
||||
let mut rows = Vec::new();
|
||||
|
||||
for (section_ix, section) in sections.iter().enumerate() {
|
||||
rows.push(InboxRow::Repo(section_ix));
|
||||
|
||||
if section.entries.is_empty() {
|
||||
rows.push(InboxRow::Empty);
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.extend(
|
||||
(0..section.entries.len()).map(|entry_ix| InboxRow::Entry(section_ix, entry_ix)),
|
||||
);
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Display name of the repository at `addr`, from the announcement store.
|
||||
fn repo_name(addr: Option<&RepoAddr>, cx: &App) -> Option<SharedString> {
|
||||
let repo_list = RepoListStore::global(cx);
|
||||
@@ -475,43 +505,6 @@ fn empty_section_row(cx: &App) -> AnyElement {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn open_item(
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
root: EventId,
|
||||
kind: Option<Kind>,
|
||||
address: Option<RepoAddr>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(address) = address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(announcement) = RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements
|
||||
.iter()
|
||||
.find(|announcement| announcement.addr() == address)
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let detail = open_repo_panel(dock_area, &announcement, window, cx);
|
||||
let Some(store) = detail.read(cx).store() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let item = match kind {
|
||||
Some(Kind::GitIssue) => RepoItem::Issue(root),
|
||||
Some(Kind::GitPullRequest) => RepoItem::PullRequest(root),
|
||||
Some(Kind::GitPatch) => RepoItem::Patch,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
open_repo_item(dock_area, store, item, window, cx);
|
||||
}
|
||||
|
||||
fn thread(
|
||||
prefix: &'static str,
|
||||
ix: usize,
|
||||
@@ -521,7 +514,6 @@ fn thread(
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let title = SharedString::from(item.title());
|
||||
let kind = item.kind().unwrap_or(Kind::Comment);
|
||||
let unread = item.is_unread();
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
@@ -530,7 +522,7 @@ fn thread(
|
||||
let mut timeline = v_flex().gap_2().w_full();
|
||||
|
||||
for event in item.timeline(MAX_SUB_ACTIVITIES) {
|
||||
timeline = timeline.child(sub_activity_line(&event, me, cx));
|
||||
timeline = timeline.child(sub_activity(&event, me, cx));
|
||||
}
|
||||
|
||||
v_flex()
|
||||
@@ -543,7 +535,7 @@ fn thread(
|
||||
.when(first, |this| this.rounded_t(cx.theme().radius))
|
||||
.when(last, |this| this.rounded_b(cx.theme().radius))
|
||||
.when(!last, |this| {
|
||||
this.border_b_1().border_color(cx.theme().border)
|
||||
this.border_b_1().border_color(cx.theme().background)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().secondary_hover.alpha(0.8)))
|
||||
.child(
|
||||
@@ -556,7 +548,7 @@ fn thread(
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(kind_icon(kind)),
|
||||
.child(Icon::new(IconName::Bell)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -579,7 +571,7 @@ fn thread(
|
||||
.child(timeline)
|
||||
}
|
||||
|
||||
fn sub_activity_line(event: &Event, me: Option<PublicKey>, cx: &App) -> AnyElement {
|
||||
fn sub_activity(event: &Event, me: Option<PublicKey>, cx: &App) -> AnyElement {
|
||||
let profile_store = ProfileStore::global(cx).read(cx);
|
||||
let profile = profile_store.get(&event.pubkey);
|
||||
|
||||
@@ -618,28 +610,6 @@ fn sub_activity_line(event: &Event, me: Option<PublicKey>, cx: &App) -> AnyEleme
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Leading icon for a notification or activity kind.
|
||||
fn kind_icon(kind: Kind) -> Icon {
|
||||
if kind == COVER_NOTE_KIND {
|
||||
return Icon::new(IconName::File).small();
|
||||
}
|
||||
|
||||
match kind {
|
||||
Kind::GitIssue => Icon::new(CustomIconName::GitIssueOngoing),
|
||||
Kind::GitPullRequest | Kind::GitPullRequestUpdate => {
|
||||
Icon::new(CustomIconName::GitPullRequest)
|
||||
}
|
||||
Kind::GitPatch => Icon::new(CustomIconName::GitCommit),
|
||||
Kind::Comment => Icon::new(IconName::Star),
|
||||
Kind::GitStatusOpen
|
||||
| Kind::GitStatusApplied
|
||||
| Kind::GitStatusClosed
|
||||
| Kind::GitStatusDraft => Icon::new(CustomIconName::GitIssueOpen),
|
||||
_ => Icon::new(IconName::Bell),
|
||||
}
|
||||
.small()
|
||||
}
|
||||
|
||||
/// Phrase describing an activity event, read as `[name] [phrase]`.
|
||||
fn activity_phrase(kind: Kind) -> &'static str {
|
||||
if kind == COVER_NOTE_KIND {
|
||||
|
||||
@@ -231,12 +231,6 @@ impl RepoDetailView {
|
||||
view
|
||||
}
|
||||
|
||||
/// The per-repository nostr store, so another panel can open one of its
|
||||
/// items. `None` until a local repository is initialized to NIP-34.
|
||||
pub(crate) fn store(&self) -> Option<Entity<RepoStore>> {
|
||||
self.store.clone()
|
||||
}
|
||||
|
||||
/// Open a local repository discovered by the scan.
|
||||
pub fn new_local(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
@@ -2660,6 +2654,11 @@ pub(crate) fn open_repo_panel(
|
||||
detail
|
||||
}
|
||||
|
||||
/// The nostr store of `announcement`'s repository, without opening a repository panel.
|
||||
fn repo_store(announcement: &Announcement, cx: &mut App) -> Entity<RepoStore> {
|
||||
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx))
|
||||
}
|
||||
|
||||
/// An item of a repository to open from outside its detail panel.
|
||||
/// A patch has no detail view in Signed, so it opens nothing.
|
||||
pub(crate) enum RepoItem {
|
||||
@@ -2668,26 +2667,34 @@ pub(crate) enum RepoItem {
|
||||
Patch,
|
||||
}
|
||||
|
||||
/// Open the detail panel of `item` in `store`'s repository, in the dock's center.
|
||||
/// Open the detail panel of `item` in `announcement`'s repository, in the dock's center.
|
||||
///
|
||||
/// The repository store is built here, not taken from a `RepoDetailView`, so the
|
||||
/// item panel is the only panel docked.
|
||||
///
|
||||
/// A patch opens nothing: patches are only consumed inside a pull request's
|
||||
/// detail panel, and have no panel of their own.
|
||||
pub(crate) fn open_repo_item(
|
||||
dock_area: &WeakEntity<DockArea>,
|
||||
store: Entity<RepoStore>,
|
||||
announcement: &Announcement,
|
||||
item: RepoItem,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let panel: Arc<dyn PanelView> = match item {
|
||||
RepoItem::Issue(issue_id) => {
|
||||
panel_handle(cx.new(|cx| IssueDetailView::new(store, issue_id, window, cx)))
|
||||
}
|
||||
RepoItem::PullRequest(pr_id) => panel_handle(
|
||||
cx.new(|cx| PullRequestDetailView::new(dock_area.clone(), store, pr_id, window, cx)),
|
||||
),
|
||||
RepoItem::Patch => return,
|
||||
};
|
||||
let panel: Arc<dyn PanelView> =
|
||||
match item {
|
||||
RepoItem::Issue(issue_id) => {
|
||||
let store = repo_store(announcement, cx);
|
||||
panel_handle(cx.new(|cx| IssueDetailView::new(store, issue_id, window, cx)))
|
||||
}
|
||||
RepoItem::PullRequest(pr_id) => {
|
||||
let store = repo_store(announcement, cx);
|
||||
panel_handle(cx.new(|cx| {
|
||||
PullRequestDetailView::new(dock_area.clone(), store, pr_id, window, cx)
|
||||
}))
|
||||
}
|
||||
RepoItem::Patch => return,
|
||||
};
|
||||
|
||||
let Some(dock_area) = dock_area.upgrade() else {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user