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:
+100
-21
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user