Repository navigation
Commit e16a7af
fix(app-shell): opening a notification from Home's My-work card marks it read, as the bell does (#27653)
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)_
Fixes #27447
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:首页「我的工作 → 通知」卡片点开一条通知时,会先把它标为已读,再跳转;与顶栏铃铛的行为一致。
- **为什么改**:之前首页卡片只跳转、不标已读,服务端未读数不变,刷新后铃铛角标仍在。
- **风险与回滚**:风险低,只动控制台前端的点击处理,铃铛行为不变;回滚即 revert 本 PR。
-
**先看**:`ui/packages/app-shell/src/layout/openInboxNotification.ts`(共享的打开动作);`HomePage.tsx`
的 `onOpenNotification`;`InboxPopover.tsx` 的 `handleNotificationClick`。
- **两分钟验证**:以有一条未读通知的成员登录首页,点「通知」卡片里的那条;预期发出 `POST
/api/v1/notifications/read`,回到首页后未读数减一、铃铛角标消失。
- **需要你做的**:无,席位落地。
## What changes
- **One open action for both doors.** New `openInboxNotification`
(`ui/packages/app-shell/src/layout/openInboxNotification.ts`): mark the
row read, resolve its `action_url` with `resolveNotificationTarget`,
then open the inbox (no link), open a new tab (external link) or go to
the console route. What differs per door is passed in: how it leaves the
page, what it does before following a link, where "open the inbox"
lands.
- **The bell** (`InboxPopover.handleNotificationClick`) now calls it
with `onMarkRead`, `leaveTo` (the unsaved-edits guard),
`goToAllNotifications` and "close the popover". Behaviour unchanged.
- **Home** (`HomePage` `onOpenNotification`) now calls it with
`useInboxBell().markRead`, the bell's own write. Before, it only
navigated. The request Home sends is the bell's `POST
/api/v1/notifications/read` `{ "ids": [notification_id] }`, keyed by the
notification event id.
Measured mechanism (premise holds): the bell's mark-read is
`useInboxBell().markRead` (AppHeader, `global:notifications`). Home's
card rows come from `useHomeInbox`, which cuts the same
`useSharedInboxFeed` but had no write. Same feed, different hook. Only
the bell's hook had the write.
## Tests
- `console/home/__tests__/HomePage.openMarksRead.test.tsx` (new). It
renders the real `HomePage` with the real `useHomeInbox`, `useInboxBell`
and shared inbox feed, over a fake data adapter and a recording `fetch`
double that writes `read` receipts when the mark-read POST arrives. It
clicks the Home card row and asserts:
- the exact request (`POST /api/v1/notifications/read`, body
`{"ids":["ntf_1"]}`),
- that the POST comes before the navigation,
- the card's unread pill going 2 → 1 once the feed re-reads,
- the linkless arm (mark read, then the inbox).
- `layout/__tests__/openInboxNotification.test.ts` (new): the four arms
of the action, as an ordered step log.
- The 14 existing `HomePage.*` harnesses stub `useInboxBell`, because
they serve no inbox feed. The bell's own pins
(`InboxPopover.notificationDeepLink`, `InboxPopover.linklessFallback`,
`InboxPopover.unsavedEditsLeave-13253`) pass unchanged.
- Ablation: replacing Home's `markRead: markNotificationRead,` with a
no-op turns all 3 cases of the new Home test red. The request list is
`[]` and the pill stays `2`. Restored from `HEAD` and verified by blob
equality.
## Acceptance notes
- **Optimistic flip is per surface, by design.** After a Home click, the
header bell's badge drops on the shared feed's next read: the 10 s poll,
or at once when the tab becomes visible again. The bell's own clicks
work the same way toward Home. `useInboxBell` keeps its optimistic
overlay per surface on purpose ("one surface's optimistic flip must not
become another's fact before the server agrees").
- **A second bell wiring is mounted on `/home`.** `HomePage` now mounts
`useInboxBell` next to the header's. Its own docblock says two mounts
are safe: the shared feed is read once, and the arrival announcer's seen
set is module-scoped, so nothing is announced twice.
- **A third door exists outside this card's file surface.** The arrival
toast and system notification (`useInboxArrivalNotifier`'s
`openMessage`) carry their own copy of the same open logic. That copy
already marks read first, so it is not a defect. Moving it onto
`openInboxNotification` would be a one-line follow-up. Noted, not
changed.
## Verification
All readings below are on head `86c3d08d`, after `origin/main` was
merged in.
<details><summary>Targeted tests and typecheck</summary>
- `pnpm --dir ui exec vitest run --maxWorkers=2
packages/app-shell/src/console/home/ packages/app-shell/src/layout/
packages/app-shell/src/hooks/__tests__/sharedInboxFeed
packages/app-shell/src/hooks/__tests__/useHomeInbox
packages/app-shell/src/views/__tests__/global-notifications`: Test Files
158 passed (158), Tests 1018 passed (1018).
- `pnpm --filter @object-ui/app-shell type-check` (src and
`tsconfig.test.json`): exit 0.
- Ablation of Home's `markRead` (via `scripts/ablation-replace.mjs`):
`HomePage.openMarksRead.test.tsx` failed 3 of 3, then was restored to
`HEAD` (blob equal, `git diff HEAD` empty).
- The whole `@object-ui/app-shell` suite (1883 files) was started
locally but stopped after 61 min. It was holding the shared verification
lock. CI's sharded `Test UI` runs it in full.
</details>
<details><summary>Derived gates (dispatch-gates --commands, reconciled
with --ran: 34 derived, 34 run, 0 not measured)</summary>
Every derived family exited 0. That includes `node
scripts/check-sdui-manifest.mjs` and its `--self-test`, the changeset
gates, `pnpm check:type-check-coverage`, `pnpm check:type-check-debt`
(re-measured under the lock) and `pnpm --dir ui exec vitest run
scripts/` (169 files passed, 2 skipped). `pnpm --dir ui
check:new-line-citations`: 0 new citations.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbDAucNyQ4UtzCJKyh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e5c5035 commit e16a7af
20 files changed
Lines changed: 528 additions & 59 deletions
File tree
- .changeset
- ui/packages/app-shell/src
- console/home
- __tests__
- layout
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
50 | 49 | | |
| 50 | + | |
| 51 | + | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| |||
369 | 370 | | |
370 | 371 | | |
371 | 372 | | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
372 | 378 | | |
373 | 379 | | |
374 | 380 | | |
| |||
833 | 839 | | |
834 | 840 | | |
835 | 841 | | |
836 | | - | |
837 | | - | |
838 | | - | |
839 | | - | |
840 | | - | |
841 | | - | |
842 | | - | |
843 | | - | |
844 | | - | |
845 | | - | |
846 | | - | |
847 | | - | |
848 | | - | |
849 | | - | |
850 | | - | |
851 | | - | |
852 | | - | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
853 | 856 | | |
854 | 857 | | |
855 | 858 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
131 | 135 | | |
132 | 136 | | |
133 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
134 | 138 | | |
135 | 139 | | |
136 | 140 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
143 | 147 | | |
144 | 148 | | |
145 | 149 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
89 | 93 | | |
90 | 94 | | |
91 | 95 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
150 | 154 | | |
151 | 155 | | |
152 | 156 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
103 | 107 | | |
104 | 108 | | |
105 | 109 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
137 | 141 | | |
138 | 142 | | |
139 | 143 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
136 | 140 | | |
137 | 141 | | |
138 | 142 | | |
| |||
0 commit comments