fix(notifications): Earlier opens on Back only for a row that can be nowhere else, and Mark read reads read at once
A namesake in Earlier while the row itself was still in Needs you or Recent opened the fold, which took the place match away and left the name twice on the page: the focus fell to <body>. Row names are trimmed as Back saves them, so a title ending in a space still matches. Mark read handed the focus to a row that still said it was unread until the write landed; the row now reads read in the press's own render, and unread again if the write is refused.
This commit is contained in:
@@ -123,11 +123,25 @@ export default function NotificationsPanel() {
|
||||
qc.invalidateQueries({ queryKey: qk.notifications() });
|
||||
qc.invalidateQueries({ queryKey: qk.notifUnread() });
|
||||
};
|
||||
// A row marked read reads read at once, in the press's own render: its Mark read button hands the
|
||||
// focus to the row, and a row still unread then was announced "…, unread" until the write landed.
|
||||
const [readNow, setReadNow] = useState<ReadonlySet<number>>(() => new Set());
|
||||
const readMut = useMutation({
|
||||
mutationFn: api.markNotificationRead,
|
||||
onSuccess: refresh,
|
||||
// Refused: the row reads unread again.
|
||||
onError: (_e, id) =>
|
||||
setReadNow((s) => {
|
||||
const next = new Set(s);
|
||||
next.delete(id);
|
||||
return next;
|
||||
}),
|
||||
meta: { failTitle: "errors.writeFailed.notifRead" },
|
||||
});
|
||||
function markRead(id: number) {
|
||||
setReadNow((s) => new Set(s).add(id));
|
||||
readMut.mutate(id);
|
||||
}
|
||||
const readAllMut = useMutation({
|
||||
mutationFn: api.markAllNotificationsRead,
|
||||
// Settled, not only succeeded: a refused write brings the rows back unread at once, rather than
|
||||
@@ -155,15 +169,19 @@ export default function NotificationsPanel() {
|
||||
// While Mark all read is on its way, a read that lands before the server commits it (the poll, or
|
||||
// another row's write refreshing the list) still says "unread": those rows are read already, or
|
||||
// the band would take them back and hold them for the rest of the visit.
|
||||
const shownServer = readAllMut.isPending
|
||||
? serverItems.map((n) => (n.read ? n : { ...n, read: true }))
|
||||
: serverItems;
|
||||
const shownServer = serverItems.map((n) =>
|
||||
n.read || !(readAllMut.isPending || readNow.has(n.id)) ? n : { ...n, read: true },
|
||||
);
|
||||
const order = orderInbox(shownServer, clientItems, { now: openedAt, held });
|
||||
const asking = order.needsYou.map(entryKey).filter((k) => !held.has(k));
|
||||
if (asking.length > 0) setHeld(new Set([...held, ...asking]));
|
||||
if (seeking && q.data) {
|
||||
setSeeking(null);
|
||||
if (order.earlier.some((e) => entryName(e, t, i18n.language) === seeking)) setEarlierOpen(true);
|
||||
// Only a row that can be nowhere else: a namesake in Earlier while the row itself is still in
|
||||
// sight would open it for nothing, and the name standing twice on the page loses the row.
|
||||
const sought = (e: InboxEntry) => entryName(e, t, i18n.language) === seeking;
|
||||
if (!order.needsYou.some(sought) && !order.recent.some(sought) && order.earlier.some(sought))
|
||||
setEarlierOpen(true);
|
||||
}
|
||||
|
||||
const serverUnread = serverItems.some((n) => !n.read);
|
||||
@@ -315,7 +333,7 @@ export default function NotificationsPanel() {
|
||||
id: "read",
|
||||
label: t("inbox.markRead"),
|
||||
icon: icon(Check),
|
||||
onSelect: () => readMut.mutate(n.id),
|
||||
onSelect: () => markRead(n.id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -404,7 +422,7 @@ export default function NotificationsPanel() {
|
||||
navIndex={i}
|
||||
rowProps={serverMenu.triggerProps(e.n)}
|
||||
more={more(serverMenu, e.n)}
|
||||
onRead={() => readMut.mutate(e.n.id)}
|
||||
onRead={() => markRead(e.n.id)}
|
||||
onDismiss={() => dismissMut.mutate(e.n.id)}
|
||||
onOpenScheduler={openScheduler}
|
||||
onOpenTakeout={openTakeout}
|
||||
@@ -685,11 +703,12 @@ function serverRowText(
|
||||
return { title, body };
|
||||
}
|
||||
|
||||
/** A row's name, for a screen reader and for Back to find it by: what it is about, not its kind. */
|
||||
/** A row's name, for a screen reader and for Back to find it by: what it is about, not its kind.
|
||||
* Trimmed as Back saves it (`lib/listNav`), or a title ending in a space never matches its row. */
|
||||
const rowName = (title: string, body: string | null | undefined): string =>
|
||||
body ? `${title}: ${body}` : title;
|
||||
(body ? `${title}: ${body}` : title).trim();
|
||||
const clientRowName = (n: ClientNotif): string =>
|
||||
n.title ? rowName(n.title, n.message) : n.message;
|
||||
n.title ? rowName(n.title, n.message) : rowName(n.message, null);
|
||||
const entryName = (e: InboxEntry, t: TFunction, lang: string): string => {
|
||||
if (e.src === "client") return clientRowName(e.n);
|
||||
const { title, body } = serverRowText(e.n, t, lang);
|
||||
|
||||
Reference in New Issue
Block a user