` (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` 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>`. 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 notification rows are clickable. Activity rows are display-only. The Phase 3 mark-read /
archive row behaviour is gone with the sub-views.
- `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).
### Repository grouping redesign (after Phase 4)
Files: `crates/workspace/src/views/inbox.rs`. No store, no `signed_core` changes.
The two-card layout (notifications over activity) was replaced by a single repository-grouped list.
- The panel now derives `sections: Vec` and a flattened `rows: Vec` in
`rebuild`, called from `run_refresh` and `regroup`. Both are stored as `Arc`s and cloned into the
`gpui::list` closure, which indexes `notifications` / `activity` - no deep copies per frame and no
data duplicated between the section list and the source lists.
- `InboxSection` groups a repository's non-archived notification groups and the user's own activity,
newest first; sections are ordered by their newest entry. `InboxEntry` holds indices into the
panel's lists; `InboxRow::Repo` / `InboxRow::Entry` / `InboxRow::Empty` is the flattened shape the
list renders.
- All of the user's own repositories are seeded as sections from `RepoListStore::announcements_of`,
so an owned repository with nothing to show gets an empty section ("No activity yet.") and sorts
after the sections with activity. The panel observes `RepoListStore` to rebuild when the user's
repositories load or change.
- Activity is matched to a repository through a `GitRepoAnnouncement` `a` tag (`repo_address`).
Items without an address share the "Other repository" section.
- `notification_row` / `activity_row` no longer render the repository name - the section header does.
That also drops one `RepoListStore` scan per row.
- The single card has one `ListState`; the old `notifications_list` / `activity_list` and the
`render_inbox_panel` / `render_activity_panel` / `section` helpers are gone. `notification_row` still
takes an id prefix so rows stay unique within the list.
- `cargo clippy -p workspace --all-targets` is clean and `cargo test -p signed_core -p signed_state
-p workspace -p dock` passes (68 / 24 / 7 / 1).
### Sidebar badge removed (after the repository grouping redesign)
Files: `crates/signed_core/src/filters.rs`, `crates/signed_state/src/{inbox.rs,backend.rs}`,
`crates/workspace/src/views/{inbox.rs,sidebar/mod.rs}`.
An intermediate change made the sidebar badge live by moving the unread count into the global
`Inbox` (a `refresh_unread_count` driven by `Backend`). That was then reverted along with the badge
itself, so the global is thin again.
- The sidebar nav item no longer renders a `CountBadge`; `SidebarPanel` lost its `unread` field and
its observe of the global `Inbox`.
- The global `Inbox` no longer stores an `unread_count` and has no `set_unread_count` /
`refresh_unread_count`. `Backend` has no `refresh_inbox_unread` and no per-batch or per-sync count
refresh. `filters::affects_inbox` and the `query_inbox` helper split were reverted with it.
- `InboxView` keeps its local `unread_count` for its header badge and the per-section `unread` for
the repository headers; `publish_unread_count` stays deleted.
- Consequence: the unread count is only current while the panel is open, and there is no unread
indication anywhere else in the app.
- `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 -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.
- `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.
- `cargo clippy -p signed_state --all-targets`, `cargo clippy -p workspace --all-targets` and
`cargo check --workspace --all-targets` after each phase.
- Manual: log in with a repo-owning identity; open the inbox from the sidebar and confirm the panel
populates from another identity's issue/comment, the activity list shows your own items, and that no
kind-30078 event is broadcast (watch the relays / `Published` events). Restart to confirm the read
state is read back from LMDB. Confirm the sidebar has no unread badge.
## 9. SDK APIs used (verified in the pinned `5c669a4` checkout)
- `Kind::{Comment, GitIssue, GitPullRequest, GitPatch, GitPullRequestUpdate,`
`GitStatusOpen/Applied/Closed/Draft, ApplicationSpecificData, EventDeletion, RequestToVanish}`
- `Filter::{kind, kinds, pubkey, pubkeys, custom_tags, limit, since, events, coordinate, identifier}`
- Non-obvious: `Filter::pubkey`/`pubkeys` set the lowercase **`p` tag**, not `authors`. Use
`Filter::author`/`authors` for authorship. The `notifications` filter relies on this.
- `SingleLetterTag::{LOWERCASE_P, LOWERCASE_E, UPPERCASE_P, UPPERCASE_K, UPPERCASE_E}`
- `nostr::nips::nip22::{extract_root, extract_parent, CommentTarget}`: NIP-22 root/parent pointers
- `Tags::{event_ids, public_keys, coordinates, identifier, hashtags}` iterators
- `Client::{database, subscribe, sync, notifications, send_event, add_relay}`;
`NostrDatabase::{save_event, query}`; `NostrLmdb`, `NostrGossipMemory`
- `EventBuilder::{new, tags, finalize}`, `Tag::identifier`, `Keys::generate`
- `Timestamp`, `EventId` (hex serde), `PublicKey`, `Coordinate`
- Fetch paths converge on the same notification: `client.subscribe(...)` and negentropy
`client.sync(...)` both persist received events to LMDB and surface them as
`ClientNotification::Event`, which `Backend`'s pump batches into `BackendEvent::NostrUpdate`.
This is why the query right after a fetch is a cache read, not a race.