From f9287081565398f8281a62abfd825931fa6c54d2 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 7 Nov 2025 00:27:57 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20animate=20exit=20and=20more?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ProductUpdates.tsx | 207 ++++++++++++++++++------------ src/lib/timeAgoFormatter.ts | 5 +- 2 files changed, 127 insertions(+), 85 deletions(-) diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx index c5f48d68..83fa32e6 100644 --- a/src/components/ProductUpdates.tsx +++ b/src/components/ProductUpdates.tsx @@ -8,7 +8,6 @@ import { useQueries } from "@tanstack/react-query"; import { ArrowRight, BellIcon, - CheckIcon, ChevronRightIcon, RocketIcon, XIcon @@ -57,7 +56,11 @@ export default function ProductUpdates({ const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage< string | null - >("ignored-version", null); + >("product-updates:skip-version", null); + + const [productUpdatesRead, setProductUpdatesRead] = useLocalStorage< + number[] + >("product-updates:read", []); if (!data) return null; @@ -68,6 +71,10 @@ export default function ProductUpdates({ env.app.version !== data.latestVersion.data?.pangolin.latestVersion ); + const filteredUpdates = data.updates.filter( + (update) => !productUpdatesRead.includes(update.id) + ); + return (
- {data.updates.length > 0 && ( + {filteredUpdates.length > 0 && ( <> {showNewVersionPopup ? t("productUpdateMoreInfo", { - noOfUpdates: data.updates.length + noOfUpdates: filteredUpdates.length }) : t("productUpdateInfo", { - noOfUpdates: data.updates.length + noOfUpdates: filteredUpdates.length })} )} 0} + updates={filteredUpdates} + show={filteredUpdates.length > 0} + onDimissAll={() => + setProductUpdatesRead([ + ...productUpdatesRead, + ...filteredUpdates.map((update) => update.id) + ]) + } + onDimiss={(id) => + setProductUpdatesRead([...productUpdatesRead, id]) + } />
{ + onDimiss={() => { setIgnoredVersionUpdate( data.latestVersion?.data?.pangolin.latestVersion ?? null ); @@ -118,29 +134,44 @@ export default function ProductUpdates({ ); } -type ProductUpdatesListPopupProps = { updates: ProductUpdate[]; show: boolean }; +type ProductUpdatesListPopupProps = { + updates: ProductUpdate[]; + show: boolean; + onDimiss: (id: number) => void; + onDimissAll: () => void; +}; function ProductUpdatesListPopup({ updates, - show + show, + onDimiss, + onDimissAll }: ProductUpdatesListPopupProps) { - const [open, setOpen] = React.useState(false); + const [showContent, setShowContent] = React.useState(false); + const [popoverOpen, setPopoverOpen] = React.useState(false); const t = useTranslations(); // we need to delay the initial opening state to have an animation on `appear` React.useEffect(() => { if (show) { - requestAnimationFrame(() => setOpen(true)); + requestAnimationFrame(() => setShowContent(true)); } }, [show]); + React.useEffect(() => { + if (updates.length === 0) { + setShowContent(false); + setPopoverOpen(false); + } + }, [updates.length]); + return ( - - + + - - - -
- - {t("productUpdateTitle")} - {updates.length} - -
-
    - {updates.map((update) => ( -
  1. -
    -

    - {update.title} - - New - -

    - - - - - - - Mark as read - - - -
    - - {update.contents} - - -
  2. - ))} -
-
+
+ +
+ + {t("productUpdateTitle")} + {updates.length > 0 && ( + {updates.length} + )} + + +
+
    + {updates.length === 0 && ( + + No updates + + )} + {updates.map((update) => ( +
  1. +
    +

    + {update.title} + {/* + {t("new")} + */} +

    + + + + + + + {t("dismiss")} + + + +
    + + {update.contents} + + +
  2. + ))} +
+
); } type NewVersionAvailableProps = { - onClose: () => void; + onDimiss: () => void; show: boolean; version: | Awaited< @@ -251,7 +294,7 @@ type NewVersionAvailableProps = { function NewVersionAvailable({ version, show, - onClose + onDimiss }: NewVersionAvailableProps) { const t = useTranslations(); const [open, setOpen] = React.useState(false); @@ -302,7 +345,7 @@ function NewVersionAvailable({ className="p-1 cursor-pointer" onClick={() => { setOpen(false); - onClose(); + onDimiss(); }} > diff --git a/src/lib/timeAgoFormatter.ts b/src/lib/timeAgoFormatter.ts index 0aeff8bc..f6ae0175 100644 --- a/src/lib/timeAgoFormatter.ts +++ b/src/lib/timeAgoFormatter.ts @@ -39,10 +39,9 @@ export function timeAgoFormatter( unit = "year"; } - const rtf = new Intl.RelativeTimeFormat("en", { + const rtf = new Intl.RelativeTimeFormat(navigator.languages[0] ?? "en", { numeric: "auto", style: short ? "narrow" : "long" }); - const formatedValue = rtf.format(-value, unit); - return formatedValue === "now" ? "Just now" : formatedValue; + return rtf.format(-value, unit); }