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 09:55:27 +07:00
parent f62fa9884d
commit 4684e01e89
5 changed files with 443 additions and 273 deletions
+100 -21
View File
@@ -7,13 +7,14 @@ Ported from GitWorkshop's home screen, the `Dashboard` rendered at route `/` for
> an account is active, and that home screen is the inbox.
> **Status.** Phases 0-4 are implemented and green on `feat/inbox`, then the screen was redesigned to
> group notifications and activity **by repository** (see the repository-grouping note in §7).
> `cargo test -p signed_core` (68), `cargo test -p signed_state` (24),
> group **threads by repository** and to merge notifications with own activity into one row per thread
> (see the repository-grouping and thread-merge notes in §7).
> `cargo test -p signed_core` (69), `cargo test -p signed_state` (24),
> `cargo test -p workspace` (7), `cargo test -p dock` (1), `cargo clippy -p workspace --all-targets`
> clean, `cargo check --workspace --all-targets` succeeds.
> Phase 5 is not started. This document reflects the implementation as it stands: the Phase 1
> refactors, the §4.3 split of the inbox into a thin global `Inbox` and a panel-owned derivation, the
> Phase 4 click-through, and the repository-grouped list. The Phase 3 bottom-dock sub-views were
> Phase 4 click-through, and the repository-grouped thread list. The Phase 3 bottom-dock sub-views were
> removed before the redesign; their implementation notes in §7 are historical.
## 1. What the GitWorkshop home screen is
@@ -72,20 +73,24 @@ Notes:
`InboxView` is a center panel, opened by the sidebar's existing **Inbox** nav item. It is one bordered
card holding a single virtual list. Every row is either a **repository header** or one of that
repository's **notifications / own activity**, newest first:
repository's **threads**, newest first. A thread merges the notifications directed at the user with
the user's own events in the same root, and shows the root's title plus up to five of its most recent
events:
The sections are **all of the user's own repositories**, seeded from `RepoListStore`, plus any other
repository that has notifications or activity. Owned repositories with nothing to show render an
repository that has threads. Owned repositories with nothing to show render an
empty state ("No activity yet.") under their header, and sort after the ones with activity (newest
announcement first). Items with no repository address fall into a single "Other repository" section.
announcement first). Threads with no repository address fall into a single "Other repository" section.
```
+-------------------------------------------------------------------------+
| Inbox (3 unread) [Mark all read] |
|-------------------------------------------------------------------------|
| [repo] you/repo-a (2) |
| [avatar] issue opened issue 2m |
| [avatar] commented on "..." comment 1h |
| [icon] "Add retry" patch 3d |
| [icon] Add retry logic (unread dot) |
| [avatar] You opened an issue 3d |
| [avatar] alice commented 2d |
| [icon] Fix flaky test |
| [avatar] You opened a PR 1h |
|-------------------------------------------------------------------------|
| [repo] you/repo-b |
| No activity yet. |
@@ -95,8 +100,8 @@ announcement first). Items with no repository address fall into a single "Other
+-------------------------------------------------------------------------+
```
The sections are the repositories that actually have notifications or activity, ordered by their
newest row. A repository the user owns but that has no items is not shown. Items with no repository
The sections are the repositories that actually have threads, ordered by their
newest row. A repository the user owns but that has no items is not shown. Threads with no repository
address fall into a single "Other repository" section.
## 4. Data layer
@@ -155,24 +160,42 @@ gitworkshop's `isGitComment`.
```rust
pub struct InboxItem {
pub root: EventId,
/// The root event itself, when known locally; drives the row title.
pub root_event: Option<Event>,
pub root_kind: Option<Kind>,
pub address: Option<RepoAddr>,
/// Events in the group, newest first.
/// Notification events directed at the user, newest first.
pub events: Vec<Event>,
/// The user's own events in the same thread, newest first.
pub own_events: Vec<Event>,
/// Unread event ids, oldest first.
pub unread_ids: Vec<EventId>,
pub archived: bool,
}
impl InboxItem {
/// Title of the thread root; falls back to the newest event it has.
pub fn title(&self) -> String;
/// Kind of the thread root; falls back to the newest event it has.
pub fn kind(&self) -> Option<Kind>;
pub fn latest_activity(&self) -> Timestamp;
/// Up to `limit` most recent events of the thread, oldest first.
pub fn timeline(&self, limit: usize) -> Vec<Event>;
pub fn is_unread(&self) -> bool;
pub fn apply_state(&mut self, state: &InboxReadState);
}
/// The thread root of a notification event, or `None` if it isn't git-related.
pub fn notification_root(
event: &Event,
lookup: &impl Fn(EventId) -> Option<Event>,
) -> Option<EventId>;
/// Group notification events by root, newest activity first, self excluded.
/// Group the notifications directed at the user together with the user's own
/// events into one item per thread, newest activity first.
pub fn group(
events: impl IntoIterator<Item = Event>,
own: impl IntoIterator<Item = Event>,
me: PublicKey,
state: &InboxReadState,
lookup: &impl Fn(EventId) -> Option<Event>,
@@ -186,7 +209,10 @@ Root resolution, ported from `getNotificationRootId`:
- NIP-22 comment (1111): uppercase `E` root pointer (SDK `nip22::extract_root`)
- PR update (1619): uppercase `E`
- statuses (1630-1633) / cover note (1624): NIP-10 root `e`
- self-authored events are excluded
- notification events authored by `me` are dropped; the user's own events are kept in
`own_events` instead, never in `events`
- `unread_ids` and `archived` are derived from `events` only, so the user's own activity is never
unread and a thread with only own events is never archived
Read/archive state, the compact high-water-mark model:
@@ -290,7 +316,7 @@ pub struct Backend {
#[derive(Default)]
pub struct Inbox {
state: InboxReadState,
state_loaded: bool,
loaded: bool,
}
impl Inbox {
@@ -302,6 +328,14 @@ impl Inbox {
pub(crate) fn activate(&mut self, me: PublicKey, client: Client, cx);
pub(crate) fn reset(&mut self, cx);
}
// inbox.rs (signed_state)
/// One item per thread, notifications and own activity merged.
pub async fn query_inbox(
client: &Client,
me: PublicKey,
state: &InboxReadState,
) -> Result<(Vec<InboxItem>, usize), Error>;
```
**The panel owns the derivation.** `InboxView` itself holds the derived lists, the copy of the read
@@ -314,8 +348,7 @@ badge only; there is no global count and no sidebar badge.
pub struct InboxView {
focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>,
notifications: Arc<Vec<InboxItem>>,
activity: Arc<Vec<Event>>,
threads: Arc<Vec<InboxItem>>, // one row per thread, merged
sections: Arc<Vec<InboxSection>>, // grouped by repository
rows: Arc<Vec<InboxRow>>, // flattened list
unread_count: usize,
@@ -599,15 +632,15 @@ whose root is not an issue/PR/patch, or whose repository is not in `RepoListStor
|---|---|
| `crates/signed_core/Cargo.toml` | add `serde` |
| `crates/signed_core/src/filters.rs` | `NOTIFICATION_KINDS`, `notification_comments`, `notifications`, `authored_activity`, `is_git_activity`, `deletions` |
| `crates/signed_core/src/inbox.rs` | **new**: `InboxItem`, `notification_root`, `group`, `InboxReadState`, tests |
| `crates/signed_core/src/inbox.rs` | **new**: `InboxItem` (root event, notifications, own events), `notification_root`, `group`, `InboxReadState`, tests |
| `crates/signed_core/src/lib.rs` | `mod inbox;` and re-exports |
| `crates/signed_state/Cargo.toml` | add `serde_json` |
| `crates/signed_state/src/inbox.rs` | thin global `Inbox` (NIP-78 read state, mark actions) and `query_inbox` (query, grouping, activity) |
| `crates/signed_state/src/inbox.rs` | thin global `Inbox` (NIP-78 read state, mark actions) and `query_inbox` (query, merge notifications + activity into threads) |
| `crates/signed_state/src/backend.rs` | `inbox: Entity<Inbox>` field, construction, `inbox()` accessor, `sync_inbox`, `RepoListStore` import |
| `crates/signed_state/src/refresh.rs` | doc comment lists `Inbox` among the `RefreshGate` users |
| `crates/signed_state/src/lib.rs` | `mod inbox;`, re-export `Inbox` and `query_inbox`; re-export `RefreshGate` (no global install) |
| `crates/dock/src/lib.rs` | `add_bottom_panel` helper (currently unused; left over from the removed sub-views) |
| `crates/workspace/src/views/inbox.rs` | `InboxView` home panel owning the derived lists, the repository grouping, and the notification click-through |
| `crates/workspace/src/views/inbox.rs` | `InboxView` home panel owning the threads, the repository grouping, and the thread click-through |
| `crates/workspace/src/views/mod.rs` | `mod inbox; pub use inbox::InboxView;`; re-export `RepoItem`, `open_repo_item`, `open_repo_panel` |
| `crates/workspace/src/views/sidebar/mod.rs` | `inbox` field, `open_inbox`, nav wiring |
| `crates/workspace/src/views/repo_detail/mod.rs` | `RepoItem`, `open_repo_item`, `RepoDetailView::store()` |
@@ -859,9 +892,55 @@ itself, so the global is thin again.
`cargo check -p signed_core -p signed_state -p workspace --all-targets` succeeds, and
`cargo test -p signed_core -p signed_state -p workspace -p dock` passes (68 / 24 / 7 / 1).
### Threads merged: notifications + activity (after the sidebar badge removal)
Files: `crates/signed_core/src/inbox.rs`, `crates/signed_state/src/inbox.rs`,
`crates/workspace/src/views/inbox.rs`.
Notifications and own activity were two separate row kinds that could describe the same thread. They
are now one item per thread: the notifications directed at the user and the user's own events in that
thread live in the same `InboxItem`. A row shows the thread root's title and up to five of the
thread's most recent events:
```
[icon] Add retry logic (unread dot)
[avatar] You opened an issue · 3d
[avatar] alice commented · 2d
```
- `InboxItem` gained `root_event: Option<Event>` and `own_events: Vec<Event>`. `events` keeps only the
notifications (others' events); `own_events` holds the user's own. `unread_ids`/`archived` are
derived from `events` alone, so own activity is never unread and a thread with only own events is
never archived (`apply_state` guards the empty case).
- New methods on `InboxItem`: `title()` (root event's subject, falling back to the newest event),
`kind()` (root kind, same fallback), and `timeline(limit)` (thread events deduplicated by id,
oldest first, always keeping the root event and filling the remaining slots with the most recent
others).
- `group` now takes both `events` (notifications) and `own` (the user's activity) and merges them on
the resolved root. Own events resolve through the same `notification_root`; an unresolved own event
becomes its own root. `query_inbox` returns `(Vec<InboxItem>, usize)` - the separate activity list
is gone, and `by_id` is extended with the own events so a comment of ours resolves to its thread.
- The panel holds `threads: Arc<Vec<InboxItem>>` instead of `notifications` + `activity`. The
`InboxEntry` enum, `entry_time`, `repo_address`, `related_activity`, `notification_row`,
`activity_row` and `kind_label` are gone. `thread_row` replaces both row kinds and is clickable like
the old notification row; `group_sections` now just buckets threads by `item.address`.
- `sub_activity_line` is unchanged and still renders `[avatar] [name] [phrase] · [ago]`, with `You`
for the signed-in user and `activity_phrase(kind)` for the verb. Rows are variable height
(`py_2`), which `gpui::list` auto-measures.
- Thread rows in a section are drawn as one stack: `render_entry` passes `first`/`last` within the
section (`entry_ix == 0` / `entry_ix + 1 == section.entries.len()`), and `thread_row` rounds the
outer edges (`rounded_t` on the first, `rounded_b` on the last, theme radius) and draws a
`border_b_1` divider on every row but the last.
- Trade-off: the row title is the thread root's, not the newest event's, so a comment thread no longer
previews the comment text. That is the point of the merge - the row identifies the thread.
- `cargo clippy -p signed_core -p signed_state -p workspace --all-targets` is clean,
`cargo check -p signed_core -p signed_state -p workspace --all-targets` succeeds, and
`cargo test -p signed_core -p signed_state -p workspace` passes (69 / 24 / 7).
## 8. Validation
- `cargo test -p signed_core` (68 tests): root resolution, grouping, read-state cutoff, serde round-trip.
- `cargo test -p signed_core` (69 tests): root resolution, grouping, merging, read-state cutoff, serde
round-trip.
- `cargo test -p signed_state` (24 tests): the `Inbox` / `query_inbox` paths that do not need GPUI
(state round-trip, grouping helpers).
- `cargo test -p workspace` (7 tests): repository-detail helpers.