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

This commit is contained in:
2026-09-12 10:27:36 +07:00
parent 4684e01e89
commit 8a229cb212
3 changed files with 158 additions and 179 deletions
+111 -141
View File
@@ -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(&sections);
let rows = self.flatten_rows(&sections);
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 {
+24 -17
View File
@@ -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;