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-11 10:46:31 +07:00
parent 1e7cf0020c
commit 568b6c0e41
6 changed files with 461 additions and 62 deletions
+118 -47
View File
@@ -6,13 +6,13 @@ Ported from GitWorkshop's home screen, the `Dashboard` rendered at route `/` for
> page. It is not. GitWorkshop's `Index` route (`src/pages/Index.tsx`) renders `<Dashboard />` when
> an account is active, and that home screen is the inbox.
> **Status.** Phases 0, 1 and 2 are implemented and green on `feat/inbox`:
> **Status.** Phases 0-4 are implemented and green on `feat/inbox`:
> `cargo test -p signed_core` (68), `cargo test -p signed_state` (24),
> `cargo test -p workspace` (7), `cargo clippy -p workspace --all-targets` clean,
> `cargo check --workspace --all-targets` succeeds.
> Phases 3-5 are not started. This document reflects the implementation as it stands, including the
> Phase 1 refactors and the §4.3 split of the inbox into a thin global `Inbox` and a
> panel-owned derivation.
> Phase 5 is not started. This document reflects the implementation as it stands, including the
> Phase 1 refactors, the §4.3 split of the inbox into a thin global `Inbox` and a panel-owned
> derivation, the Phase 3 bottom-dock sub-views, and the Phase 4 click-through.
## 1. What the GitWorkshop home screen is
@@ -479,16 +479,17 @@ definite viewport height. The list counts are reset from `render` whenever the r
changes. Row ids are prefixed (`("inbox-row", ix)` / `("activity-row", ix)`) so the two lists do not
collide.
- **Inbox card**: header with the unread count badge and **Mark all read**; then every non-archived
notification item. Rows show the actor avatar, a kind icon, the subject, the kind label, the repo
name, a relative time, and an unread dot (the subject is semibold while unread). Empty state:
"You're all caught up." with `IconName::Inbox`.
- **Inbox card**: header with the unread count badge, the **Unread** and **Archived** sub-view buttons
(Phase 3) and **Mark all read**; then every non-archived notification item. Rows show the actor
avatar, a kind icon, the subject, the kind label, the repo name, a relative time, and an unread dot
(the subject is semibold while unread). Empty state: "You're all caught up." with
`IconName::Inbox`.
- **Continue where you left off**: every event in the activity list, each row a kind icon, subject,
kind label, repo name, and relative time.
No greeting header, and no **My repositories** column - the sidebar already lists the user's
repositories. The **Unread** and **Archived** header buttons belong to Phase 3 and are not
rendered until `add_bottom_panel` / `InboxFilterView` exist (see 5.2).
repositories. The **Unread** and **Archived** header buttons open the sub-views of 5.2 in the bottom
dock.
### 5.2 Unread / Archived as bottom-dock panels
@@ -508,12 +509,20 @@ pub fn add_bottom_panel(
The workspace already supports a bottom dock and prunes it when empty (`workspace.rs`). Then:
- New `InboxFilterView` panel taking a mode `InboxFilter::Unread | InboxFilter::Archived` and the
`Entity<InboxView>`. It renders the matching subset of the panel's notifications as a list.
- The **Unread** and **Archived** header buttons in `InboxView` (added in Phase 3) call
`add_bottom_panel` with the requested mode. `InboxView` keeps
`filter_view: Option<WeakEntity<InboxFilterView>>`; when it already exists, update its mode and
focus instead of adding a duplicate. Until then the inbox header has only **Mark all read**.
- `InboxFilterView` is a bottom-dock panel holding an `Entity<InboxView>` and a mode
`InboxFilter::Unread | InboxFilter::Archived`. It renders the matching subset of the panel's
notifications as a `gpui::list`, with the same rows as the inbox card. The mode filters on
`InboxItem::is_unread()` / `InboxItem::archived` and picks the tab title and empty state. It reads
the inbox entity during render, so GPUI's render-time tracking re-renders it whenever the inbox
re-derives; it needs no subscription of its own.
- The **Unread** and **Archived** header buttons in `InboxView` call `InboxView::open_filter`, which
keeps `filter_view: Option<WeakEntity<InboxFilterView>>`. When the panel already exists it updates
its mode, focuses it, and reopens the bottom dock if the user collapsed it, instead of adding a
duplicate. Otherwise it creates the panel and adds it to the bottom dock.
- **Unread rows**: clicking a row marks that group read (`InboxView::mark_read`); a trailing ghost
icon button archives it (`InboxView::mark_archived`). Both call back into the `InboxView` entity,
which updates the global `Inbox`. **Archived rows** are display-only, since the read state has no
un-archive operation.
### 5.3 Sidebar
@@ -522,6 +531,7 @@ In `views/sidebar/mod.rs`:
- Add `inbox: Option<WeakEntity<InboxView>>` (mirrors `explore`) and `unread: usize`.
- Add `fn open_inbox(&mut self, window, cx)` that returns when the panel is already open, else adds
a center panel (same shape as `open_explore`; there is no dock API to focus an existing tab).
`InboxView::new` takes the sidebar's `WeakEntity<DockArea>` so the panel can open its sub-view.
- Point the existing nav item at it and add an unread suffix:
```rust
@@ -535,34 +545,39 @@ In `views/sidebar/mod.rs`:
### 5.4 Click-through (P1)
Reuse the single `RepoStore` that `RepoDetailView` already creates instead of making a second one:
Reuse the single `RepoStore` that `RepoDetailView` already creates instead of making a second one.
The detail panels need a `Window`, and GPUI's `Entity::update_in` only exists on a `VisualContext`,
which a synchronous `App` + `Window` pair is not - so the entry point is a free function rather than
a `RepoDetailView::open_item` method. In `repo_detail/mod.rs`:
1. In `repo_detail/mod.rs`, add:
```rust
pub(crate) enum RepoItem {
Issue(EventId),
PullRequest(EventId),
Patch,
}
```rust
pub(crate) enum RepoItem {
Issue(EventId),
PullRequest(EventId),
Patch(EventId),
}
pub(crate) fn open_repo_item(
dock_area: &WeakEntity<DockArea>,
store: Entity<RepoStore>,
item: RepoItem,
window: &mut Window,
cx: &mut App,
) { /* new IssueDetailView / PullRequestDetailView, added to the center */ }
```
impl RepoDetailView {
pub(crate) fn open_item(
&mut self,
item: RepoItem,
window: &mut Window,
cx: &mut Context<Self>,
) { /* open IssueDetailView / PullRequestDetailView in the dock */ }
}
```
2. `open_repo_panel` already returns `Entity<RepoDetailView>`; the caller invokes
`detail.update_in(window, cx, |detail, window, cx| detail.open_item(...))`.
3. `InboxView` resolves `item.address` to an `Announcement` from `RepoListStore`, opens the repo
panel, then calls `open_item` with the root id and kind.
- `RepoDetailView::store()` exposes its `Option<Entity<RepoStore>>`, so the caller reuses the repo
panel's store rather than building one. `views/mod.rs` re-exports `RepoItem` and `open_repo_item`.
- `InboxView` resolves `item.address` to an `Announcement` from `RepoListStore`, calls
`open_repo_panel` (which returns `Entity<RepoDetailView>`), takes its store, and calls
`open_repo_item` with the root id and kind. The detail panel renders a "not found" placeholder
until the store's fetch lands, then re-renders.
- The repository panel and the detail panel are two tabs of the center group; the detail is
activated. This matches the sidebar, which also opens a fresh repo panel per click.
Patches have no detail view in Signed (they are only consumed inside `PullRequestDetailView`), so a
patch-root click opens the repo panel. Note as a known limitation.
patch-root click opens the repo panel only. `RepoItem::Patch` carries no id for that reason. A group
whose root is not an issue/PR/patch, or whose repository is not in `RepoListStore`, opens nothing.
## 6. File-by-file change list
@@ -578,13 +593,10 @@ patch-root click opens the repo panel. Note as a known limitation.
| `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 |
| `crates/workspace/src/views/inbox.rs` | **new**: `InboxView` home panel owning the derived lists directly (`InboxFilterView` is Phase 3) |
| `crates/workspace/src/views/mod.rs` | `mod inbox; pub use inbox::InboxView;` |
| `crates/workspace/src/views/sidebar/mod.rs` | `inbox`/`unread` fields, `open_inbox`, nav wiring and badge, `create_repo_dialog` visibility |
| `crates/workspace/src/views/repo_detail/mod.rs` | `RepoItem`, `RepoDetailView::open_item` (P1) |
`create_repo_dialog` changes from private (`mod`) to `pub(crate) mod` inside `sidebar`, so the inbox's
New button can open it.
| `crates/workspace/src/views/inbox.rs` | **new**: `InboxView` home panel owning the derived lists directly, the `InboxFilterView` bottom-dock sub-view for Unread / Archived, and the notification 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`/`unread` fields, `open_inbox`, nav wiring and badge |
| `crates/workspace/src/views/repo_detail/mod.rs` | `RepoItem`, `open_repo_item`, `RepoDetailView::store()` |
No changes to `desktop` or `signed_nostr`. `signed_state::init` gains no parameters; `Backend::sync_inbox`
activates the `Inbox` child entity at each signer transition.
@@ -604,7 +616,9 @@ activates the `Inbox` child entity at each signer transition.
3. **Phase 2 - screen**: `InboxView` (inbox + activity), sidebar nav and badge.
**DONE.** See the implementation notes below.
4. **Phase 3 - sub-views**: `add_bottom_panel` and `InboxFilterView` for Unread / Archived.
5. **Phase 4 - click-through**: `open_item` and announcement lookup.
**DONE.** See the implementation notes below.
5. **Phase 4 - click-through**: `open_item` and announcement lookup. **DONE.** See the implementation
notes below.
6. **Phase 5 (optional)**: standalone notifications page, NIP-65 relays, pagination, patch detail
view.
@@ -720,6 +734,63 @@ The `InboxStore` entity was then folded into `InboxView`, since the panel was it
Trade-off: the sidebar badge is only current after the inbox is opened once, because the unread
count is derived by the panel.
### Phase 3 implementation notes
Files: `crates/dock/src/lib.rs` and `crates/workspace/src/views/{inbox.rs, sidebar/mod.rs}`. No
store changes.
- `add_bottom_panel` sits next to `add_center_panel` and wraps
`DockArea::add_panel_view(panel, DockPlacement::Bottom, None, ...)`. A new bottom dock starts open,
and the workspace's existing `DockEvent::LayoutChanged` subscription removes an emptied bottom dock,
so a closed sub-view leaves no strip behind.
- `InboxFilterView` is private to `views/inbox.rs`. It holds an `Entity<InboxView>` (strong; the
panel keeps only the weak `filter_view` back, so there is no cycle), the mode, and its own
`ListState`. There is no subscription: it reads the inbox entity during render, which is enough for
GPUI to invalidate the window when the inbox notifies.
- `InboxFilter` is a private two-variant enum with `label()` and `matches(&InboxItem)`. The tab title
comes from `Panel::title`, so switching modes through `set_mode` retitles the same tab instead of
opening a second one.
- `InboxView` regained a `dock_area: WeakEntity<DockArea>` (removed with the My-repositories column)
and takes it in `new`. `open_filter` reuses the existing panel, focuses it, and reopens the bottom
dock when it is collapsed; otherwise it creates and adds the panel. `InboxView::new` is now called
as `InboxView::new(self.dock_area.clone(), cx)` from `SidebarPanel::open_inbox`.
- The three `#[allow(dead_code)]` markers on `mark_read`, `mark_archived` and `group_events` are gone:
Unread rows call `mark_read` on click and `mark_archived` from a trailing ghost icon button
(`Button` + `IconName::FolderClosed`, tooltip "Archive"). The button calls `cx.stop_propagation()`
so it does not also trigger the row's mark-read click. Archived rows are display-only; the read
state has no un-archive operation.
- `notification_row` takes an id `prefix` and returns `Stateful<Div>` rather than `AnyElement`, so
callers can attach a click handler and a trailing action. The inbox list passes `"inbox-row"` and
the sub-view `"inbox-filter-row"`, because the two lists render in the same window and would
otherwise collide on `(str, ix)` ids.
- `cargo clippy -p workspace -p dock --all-targets` is clean, `cargo check --workspace --all-targets`
succeeds, and `cargo test -p signed_core -p signed_state -p workspace` passes (68 / 24 / 7).
### Phase 4 implementation notes
Files: `crates/workspace/src/views/{inbox.rs, mod.rs, repo_detail/mod.rs}`. No store changes.
- `RepoItem { Issue(EventId), PullRequest(EventId), Patch }` and `pub(crate) fn open_repo_item` live
in `repo_detail/mod.rs`, next to `open_repo_panel`. `open_repo_item` takes the store as a
parameter, avoiding a second `RepoStore`.
- It is a free function, not `RepoDetailView::open_item`: the detail constructors take a `Window`, and
a synchronous `&mut App` + `&mut Window` pair is not a `VisualContext`, so `Entity::update_in` is
not available. `InboxView` already has the window in the list's `on_click`, so it drives the free
function directly. The plan's original `detail.update_in(window, cx, ...)` sketch could not compile.
- `RepoDetailView::store()` (`pub(crate)`) exposes the panel's `Option<Entity<RepoStore>>`. The repo
panel is opened first and its store reused, so the detail panel shares one store with the repo it
came from.
- `InboxView::open_item` is also a free function (it needs nothing but `dock_area`, which it captures
from the panel) because the `gpui::list` item closure only receives `&mut App`. It resolves
`item.address` through `RepoListStore`, returns silently when the repository is unknown, opens the
repo panel, then maps the root kind to a `RepoItem` and calls `open_repo_item`.
- Only the main notification list is clickable. Unread rows keep their Phase 3 behaviour (click marks
read, trailing button archives). Activity rows are unchanged.
- `RepoItem::Patch` is a unit variant because the id would be unused: patches have no detail panel, so
`open_repo_item` returns before doing anything and only the repository panel opens.
- `cargo clippy -p workspace --all-targets` is clean, `cargo check --workspace --all-targets` succeeds,
and `cargo test -p signed_core -p signed_state -p workspace -p dock` passes (68 / 24 / 7 / 1).
## 8. Validation
- `cargo test -p signed_core` (68 tests): root resolution, grouping, read-state cutoff, serde round-trip.