"use client"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useLocalStorage } from "@app/hooks/useLocalStorage"; import { cn } from "@app/lib/cn"; import { type LatestVersionResponse, type ProductUpdate, productUpdatesQueries } from "@app/lib/queries"; import { useQueries } from "@tanstack/react-query"; import { ArrowRight, BellIcon, ChevronRightIcon, ExternalLinkIcon, RocketIcon, XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { Transition } from "@headlessui/react"; import * as React from "react"; import { gt, valid } from "semver"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Button } from "./ui/button"; import { Badge } from "./ui/badge"; import { timeAgoFormatter } from "@app/lib/timeAgoFormatter"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; export default function ProductUpdates({ isCollapsed }: { isCollapsed?: boolean; }) { const { env } = useEnvContext(); const data = useQueries({ queries: [ productUpdatesQueries.list(env.app.notifications.product_updates), productUpdatesQueries.latestVersion( env.app.notifications.new_releases ) ], combine(result) { if (result[0].isLoading || result[1].isLoading) return null; return { updates: result[0].data?.data ?? [], latestVersion: result[1].data }; } }); const t = useTranslations(); const [showMoreUpdatesText, setShowMoreUpdatesText] = React.useState(false); // we delay the small text animation so that the user can notice it React.useEffect(() => { const timeout = setTimeout(() => setShowMoreUpdatesText(true), 600); return () => clearTimeout(timeout); }, []); const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage< string | null >("product-updates:skip-version", null); const [productUpdatesRead, setProductUpdatesRead] = useLocalStorage< number[] >("product-updates:read", []); if (!data) return null; const latestVersion = data?.latestVersion?.data?.pangolin.latestVersion; const currentVersion = env.app.version; const showNewVersionPopup = Boolean( latestVersion && valid(latestVersion) && valid(currentVersion) && ignoredVersionUpdate !== latestVersion && gt(latestVersion, currentVersion) ); const filteredUpdates = data.updates.filter( (update) => !productUpdatesRead.includes(update.id) ); return (
{filteredUpdates.length > 1 && ( <> {showNewVersionPopup ? t("productUpdateMoreInfo", { noOfUpdates: filteredUpdates.length }) : t("productUpdateInfo", { noOfUpdates: filteredUpdates.length })} )} 0} onDimissAll={() => setProductUpdatesRead([ ...productUpdatesRead, ...filteredUpdates.map((update) => update.id) ]) } onDimiss={(id) => setProductUpdatesRead([...productUpdatesRead, id]) } />
{ setIgnoredVersionUpdate( data.latestVersion?.data?.pangolin.latestVersion ?? null ); }} show={showNewVersionPopup} />
); } type ProductUpdatesListPopupProps = { updates: ProductUpdate[]; show: boolean; onDimiss: (id: number) => void; onDimissAll: () => void; }; function ProductUpdatesListPopup({ updates, show, onDimiss, onDimissAll }: ProductUpdatesListPopupProps) { 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(() => setShowContent(true)); } }, [show]); React.useEffect(() => { if (updates.length === 0) { setShowContent(false); setPopoverOpen(false); } }, [updates.length]); return (

{t("productUpdateWhatsNew")}

{updates[0]?.contents}
{t("productUpdateTitle")} {updates.length > 0 && ( {updates.length} )}
    {updates.length === 0 && ( {t("productUpdateEmpty")} )} {updates.map((update) => (
  1. {update.title} {update.type}

    {t("dismiss")}
    {update.contents}{" "} {update.link && ( Read more{" "} )}
  2. ))}
); } type NewVersionAvailableProps = { onDimiss: () => void; show: boolean; version: LatestVersionResponse | null | undefined; }; function NewVersionAvailable({ version, show, onDimiss }: NewVersionAvailableProps) { const t = useTranslations(); const [open, setOpen] = React.useState(false); // we need to delay the initial opening state to have an animation on `appear` React.useEffect(() => { if (show) { requestAnimationFrame(() => setOpen(true)); } }, [show]); return (
{version && ( <>

{t("pangolinUpdateAvailable")}

{t("pangolinUpdateAvailableInfo", { version: version.pangolin.latestVersion })} {t("pangolinUpdateAvailableReleaseNotes")}
)}
); }