Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/27447-home-notification-marks-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/app-shell': patch
'@object-ui/console': patch
---

fix(app-shell): opening a notification from Home's "My work → Notifications" card marks it read, as the bell does

Clause-②: no
Metadata-protocol: none

Clicking a notification in the top-bar bell marked it read and then opened its link. Clicking the same notification on the Home page's "My work → Notifications" card only opened the link: no `POST /api/v1/notifications/read` was sent, the server's `unreadCount` did not change, and the bell kept its badge after a reload.

Both places now use one open action, `openInboxNotification`. It marks the notification read first, then opens its link, or opens the inbox when the notification has no link. The Home card marks read through the bell's own write (`useInboxBell().markRead`), so it sends the same request, keyed by the notification's event id. Once the inbox feed re-reads, the Home card's unread count and the bell's badge drop.
39 changes: 21 additions & 18 deletions ui/packages/app-shell/src/console/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ import {
matchAppBySegment,
resolveHostAppSegment,
resolveKeyedI18nLabel,
resolveNotificationTarget,
} from '../../utils/index.js';
import { useInboxBell } from '../../hooks/useInboxBell.js';
import { openInboxNotification } from '../../layout/openInboxNotification.js';
import { resolveSetupAppPath } from '../ConsoleShell.js';
import {
Empty,
Expand Down Expand Up @@ -369,6 +370,11 @@ export function HomePage() {
notificationsReadable,
activity,
} = useHomeInbox();
// Opening a notification from the My-work card marks it read through the
// bell's own write — `useInboxBell` is the one wiring of mark-read onto the
// shared inbox feed (the header bell and the `global:notifications` block
// mount it too), so the request Home sends is the request the bell sends.
const { markRead: markNotificationRead } = useInboxBell();
// The drafts row of the My-work section — `null` with nothing to publish.
const draftsRow = usePendingDraftsRow(t, language);
// Home renders OUTSIDE the `/apps/:appName/*` router, so there is no
Expand Down Expand Up @@ -833,23 +839,20 @@ export function HomePage() {
so it has to be hosted under an app before it is a route —
navigating it verbatim is objectui#5179, where the console
catch-all forwarded the unmatched path to `/` and the landing
resolver dropped the user on the default app's home with the
notification already marked read. `resolveNotificationTarget`
is shared with the top-bar bell so the two cannot answer this
differently. The fallback arm runs whenever a notification
carries no link at all, and opens the inbox. */
onOpenNotification={(n) => {
const target = resolveNotificationTarget(n.actionUrl, hostAppSegment);
if (!target) {
navigate(`/apps/${hostAppSegment}/sys_inbox_message?view=mine`);
return;
}
if (target.kind === 'external') {
window.open(target.url, '_blank', 'noopener,noreferrer');
return;
}
navigate(target.path);
}}
resolver dropped the user on the default app's home.
`openInboxNotification` is the top-bar bell's own open action,
so the two doors cannot answer differently: it marks the
notification read first (this row used to skip that, and a
notification opened from Home stayed unread on the server and
badged on the bell), then follows the resolved link — or, when
the notification carries no link at all, opens the inbox. */
onOpenNotification={(n) =>
openInboxNotification(n, hostAppSegment, {
markRead: markNotificationRead,
goTo: (path) => navigate(path),
openInbox: () => navigate(`/apps/${hostAppSegment}/sys_inbox_message?view=mine`),
})
}
t={t}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ vi.mock('../../../preview/usePublishAllDrafts', () => ({
}));

import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config';
// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ vi.mock('../../../runtime-config', () => ({
isAiStudioEnabled: () => true,
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ vi.mock('../../../runtime-config', () => ({
isAiStudioEnabled: () => true,
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ vi.mock('../../../runtime-config', () => ({
isAiStudioEnabled: () => true,
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

const DRAFTS_ROUTE = '/api/v1/meta/_drafts';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ vi.mock('../../../preview/usePublishAllDrafts', () => ({
usePublishAllDrafts: () => ({ publishAll: async () => ({ ok: true }), publishing: false }),
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

const CRM = { name: 'crm', label: 'CRM', icon: 'Building2' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ vi.mock('../../../runtime-config', () => ({
isAiStudioEnabled: () => true,
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/* ── The `_drafts` double ─────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ vi.mock('../../../runtime-config', () => ({
isAiStudioEnabled: () => true,
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ vi.mock('../../../preview/usePublishAllDrafts', () => ({
usePublishAllDrafts: () => ({ publishAll: async () => ({ ok: true }), publishing: false }),
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';
import { LauncherAppsProvider } from '../../../hooks/useLauncherApps';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ vi.mock('../../../preview/usePublishAllDrafts', () => ({
}));

import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config';
// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/** A `GET /api/v1/runtime/config` answer, as the server sends it. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ vi.mock('../../../views/studio-design/packages-io', async (importOriginal) => ({
fetchPackages: () => (state.packages ? Promise.resolve(state.packages) : new Promise<never>(() => {})),
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

const CRM = { name: 'crm', label: 'CRM', icon: 'Building2' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ vi.mock('../../../preview/usePublishAllDrafts', () => ({
usePublishAllDrafts: () => ({ publishAll: async () => ({ ok: true }), publishing: false }),
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';
import { STUDIO_NEW_PACKAGE_PATH } from '../../../views/studio-design/studioNewPackage';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ vi.mock('../../../runtime-config', () => ({
isAiStudioEnabled: () => true,
}));

// Home marks an opened notification read through the bell's own write
// (`useInboxBell`); this harness serves no inbox feed, so the write is stubbed.
vi.mock('../../../hooks/useInboxBell', () => ({ useInboxBell: () => ({ markRead: vi.fn() }) }));

import { HomePage } from '../HomePage';

/* ── The `_drafts` double (objectui#7307) ─────────────────────────────────────
Expand Down
Loading
Loading