fix(notifications): Back to a row that folded away since opens Earlier on it
This commit is contained in:
@@ -59,7 +59,7 @@ import { useFeedFilters, useFeedFiltersActions } from "../feed";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { SectionLabel } from "../../components/ui/Headings";
|
||||
import NavGrid from "../../components/ui/NavGrid";
|
||||
import { focusNavItem, navItemAttrs } from "../../lib/listNav";
|
||||
import { focusNavItem, navItemAttrs, pendingNavName } from "../../lib/listNav";
|
||||
import { useContextMenu, type ContextMenuController } from "../../components/ui/useContextMenu";
|
||||
import { ContextMenu } from "../../components/ui/ContextMenu";
|
||||
import { MoreButton } from "../../components/ui/MoreButton";
|
||||
@@ -79,7 +79,7 @@ const POLL_MS = 8000;
|
||||
const byId = (n: { id: number }) => n.id;
|
||||
|
||||
export default function NotificationsPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { setPage } = useNavigationActions();
|
||||
const { openFile } = useFilesActions();
|
||||
const filters = useFeedFilters();
|
||||
@@ -108,6 +108,10 @@ export default function NotificationsPanel() {
|
||||
);
|
||||
const [openedAt] = useState(() => Date.now());
|
||||
const [earlierOpen, setEarlierOpen] = useState(false);
|
||||
// Back to a row that folded away since it was opened (read on that visit, and over a week old):
|
||||
// Earlier opens on it, or the restore finds no list holding it and the focus falls on the row now
|
||||
// at its old place. Read once, as the center opens; settled once the server's list is in.
|
||||
const [seeking, setSeeking] = useState(pendingNavName);
|
||||
|
||||
// Opening the center marks the (ephemeral) client items read, matching the old bell. Server
|
||||
// items stay unread until acted on individually.
|
||||
@@ -157,6 +161,10 @@ export default function NotificationsPanel() {
|
||||
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);
|
||||
}
|
||||
|
||||
const serverUnread = serverItems.some((n) => !n.read);
|
||||
const hasAny = serverItems.length > 0 || clientItems.length > 0;
|
||||
@@ -614,36 +622,12 @@ function RowEnd({
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationRow({
|
||||
n,
|
||||
t,
|
||||
navIndex,
|
||||
rowProps,
|
||||
more,
|
||||
onRead,
|
||||
onDismiss,
|
||||
onOpenScheduler,
|
||||
onOpenTakeout,
|
||||
onOpenFile,
|
||||
onOpenTokens,
|
||||
}: {
|
||||
n: AppNotification;
|
||||
t: TFunction;
|
||||
navIndex: number;
|
||||
rowProps: Record<string, unknown>;
|
||||
more: ReactNode;
|
||||
onRead: () => void;
|
||||
onDismiss: () => void;
|
||||
onOpenScheduler: () => void;
|
||||
onOpenTakeout: () => void;
|
||||
onOpenFile: () => void;
|
||||
onOpenTokens: () => void;
|
||||
}) {
|
||||
const lang = useTranslation().i18n.language;
|
||||
// The maintenance producer ships the affected videos in `data.videos`; list a few titles.
|
||||
const videos: { id: string; title: string | null }[] = Array.isArray(n.data?.videos)
|
||||
? n.data!.videos
|
||||
: [];
|
||||
/** What a server row says: its title and body — in the reader's language for a known type. */
|
||||
function serverRowText(
|
||||
n: AppNotification,
|
||||
t: TFunction,
|
||||
lang: string,
|
||||
): { title: string; body: string | null } {
|
||||
// Known notification types are rendered from i18n (so they're trilingual) using the typed
|
||||
// payload; the server-stored title/body are an English fallback for any unknown type.
|
||||
const isMaintenance = n.type === "maintenance" && typeof n.data?.count === "number";
|
||||
@@ -698,9 +682,58 @@ function NotificationRow({
|
||||
title = t(DONE_TITLE[d.kind ?? ""] ?? "inbox.downloadDone.title");
|
||||
body = d.title || n.body;
|
||||
}
|
||||
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. */
|
||||
const rowName = (title: string, body: string | null | undefined): string =>
|
||||
body ? `${title}: ${body}` : title;
|
||||
const clientRowName = (n: ClientNotif): string =>
|
||||
n.title ? rowName(n.title, n.message) : n.message;
|
||||
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);
|
||||
return rowName(title, body);
|
||||
};
|
||||
|
||||
function NotificationRow({
|
||||
n,
|
||||
t,
|
||||
navIndex,
|
||||
rowProps,
|
||||
more,
|
||||
onRead,
|
||||
onDismiss,
|
||||
onOpenScheduler,
|
||||
onOpenTakeout,
|
||||
onOpenFile,
|
||||
onOpenTokens,
|
||||
}: {
|
||||
n: AppNotification;
|
||||
t: TFunction;
|
||||
navIndex: number;
|
||||
rowProps: Record<string, unknown>;
|
||||
more: ReactNode;
|
||||
onRead: () => void;
|
||||
onDismiss: () => void;
|
||||
onOpenScheduler: () => void;
|
||||
onOpenTakeout: () => void;
|
||||
onOpenFile: () => void;
|
||||
onOpenTokens: () => void;
|
||||
}) {
|
||||
const lang = useTranslation().i18n.language;
|
||||
// The maintenance producer ships the affected videos in `data.videos`; list a few titles.
|
||||
const videos: { id: string; title: string | null }[] = Array.isArray(n.data?.videos)
|
||||
? n.data!.videos
|
||||
: [];
|
||||
const { title, body } = serverRowText(n, t, lang);
|
||||
const isTakeout = serverKind(n) === "takeout";
|
||||
const isScheduler = serverKind(n) === "scheduler" || isTakeout;
|
||||
const isDownloadDone = serverKind(n) === "download";
|
||||
const isTokenExpiring = serverKind(n) === "token";
|
||||
// Named by what it is about, not its kind alone: every "Download ready" read the same to a screen
|
||||
// reader, and Back could not tell the row it was opened from by its name.
|
||||
const name = body ? `${title}: ${body}` : title;
|
||||
const name = rowName(title, body);
|
||||
return (
|
||||
<div
|
||||
// The unread state is spoken too (the dot is only drawn), apart from the name Back finds it by.
|
||||
@@ -802,7 +835,7 @@ function ClientActivityRow({
|
||||
const access = n.meta?.kind === "access-requests" ? n.meta : null;
|
||||
return (
|
||||
<div
|
||||
{...navItemAttrs(navIndex, n.title ? `${n.title}: ${n.message}` : n.message)}
|
||||
{...navItemAttrs(navIndex, clientRowName(n))}
|
||||
{...rowProps}
|
||||
className={`${SURFACE.card} group p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user