From 474f64293e84a5bc6a7366f934eac0ba57b3eac6 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 8 Nov 2025 16:05:46 +0000 Subject: feat: Add what's new modal in the sidebar (#2099) * Refactor sidebar release modal to use React Query * fixes * fix manual server override --- apps/web/components/shared/sidebar/Sidebar.tsx | 15 +- .../components/shared/sidebar/SidebarVersion.tsx | 250 +++++++++++++++++++++ 2 files changed, 252 insertions(+), 13 deletions(-) create mode 100644 apps/web/components/shared/sidebar/SidebarVersion.tsx (limited to 'apps/web/components') diff --git a/apps/web/components/shared/sidebar/Sidebar.tsx b/apps/web/components/shared/sidebar/Sidebar.tsx index 21d3ea48..bf5a626b 100644 --- a/apps/web/components/shared/sidebar/Sidebar.tsx +++ b/apps/web/components/shared/sidebar/Sidebar.tsx @@ -1,10 +1,10 @@ -import Link from "next/link"; import { useTranslation } from "@/lib/i18n/server"; import { TFunction } from "i18next"; import serverConfig from "@karakeep/shared/config"; import SidebarItem from "./SidebarItem"; +import SidebarVersion from "./SidebarVersion"; import { TSidebarItem } from "./TSidebarItem"; export default async function Sidebar({ @@ -32,18 +32,7 @@ export default async function Sidebar({ {extraSections} - - Karakeep v{serverConfig.serverVersion} - + ); } diff --git a/apps/web/components/shared/sidebar/SidebarVersion.tsx b/apps/web/components/shared/sidebar/SidebarVersion.tsx new file mode 100644 index 00000000..fc2ec5a3 --- /dev/null +++ b/apps/web/components/shared/sidebar/SidebarVersion.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { MarkdownReadonly } from "@/components/ui/markdown/markdown-readonly"; +import { useClientConfig } from "@/lib/clientConfig"; +import { useTranslation } from "@/lib/i18n/client"; +import { useQuery } from "@tanstack/react-query"; +import { AlertCircle, Loader2 } from "lucide-react"; +import { z } from "zod"; + +const GITHUB_OWNER_REPO = "karakeep-app/karakeep"; +const GITHUB_REPO_URL = `https://github.com/${GITHUB_OWNER_REPO}`; +const GITHUB_RELEASE_URL = `${GITHUB_REPO_URL}/releases/tag/`; +const RELEASE_API_URL = `https://api.github.com/repos/${GITHUB_OWNER_REPO}/releases/tags/`; +const LOCAL_STORAGE_KEY = "karakeep:whats-new:last-seen-version"; +const RELEASE_NOTES_STALE_TIME = 1000 * 60 * 10; // 10 minutes + +const zGitHubReleaseSchema = z.object({ + body: z.string().optional(), + tag_name: z.string(), + name: z.string(), +}); + +function isStableRelease(version?: string) { + if (!version) { + return false; + } + const normalized = version.toLowerCase(); + if ( + normalized.includes("nightly") || + normalized.includes("beta") || + normalized.includes("0.0.1") + ) { + return false; + } + return true; +} + +interface SidebarVersionProps { + serverVersion?: string; +} + +export default function SidebarVersion({ serverVersion }: SidebarVersionProps) { + const { disableNewReleaseCheck } = useClientConfig(); + const { t } = useTranslation(); + + const stableRelease = isStableRelease(serverVersion); + const displayVersion = serverVersion ?? "unknown"; + const versionLabel = `Karakeep v${displayVersion}`; + const releasePageUrl = useMemo(() => { + if (!serverVersion || !isStableRelease(serverVersion)) { + return GITHUB_REPO_URL; + } + return `${GITHUB_RELEASE_URL}v${serverVersion}`; + }, [serverVersion]); + + const [open, setOpen] = useState(false); + const [shouldNotify, setShouldNotify] = useState(false); + + const releaseNotesQuery = useQuery({ + queryKey: ["sidebar-release-notes", serverVersion], + queryFn: async ({ signal }) => { + if (!serverVersion) { + return ""; + } + + const response = await fetch(`${RELEASE_API_URL}v${serverVersion}`, { + signal, + }); + + if (!response.ok) { + throw new Error("Failed to load release notes"); + } + + const json = await response.json(); + const data = zGitHubReleaseSchema.parse(json); + return data.body ?? ""; + }, + enabled: + open && + stableRelease && + !disableNewReleaseCheck && + Boolean(serverVersion), + staleTime: RELEASE_NOTES_STALE_TIME, + retry: 1, + refetchOnWindowFocus: false, + }); + + const isLoadingReleaseNotes = + releaseNotesQuery.isLoading && !releaseNotesQuery.data; + + const releaseNotesErrorMessage = useMemo(() => { + const queryError = releaseNotesQuery.error; + if (!queryError) { + return null; + } + + const errorName = + queryError instanceof Error + ? queryError.name + : typeof (queryError as { name?: unknown })?.name === "string" + ? String((queryError as { name?: unknown }).name) + : undefined; + + if ( + errorName === "AbortError" || + errorName === "CanceledError" || + errorName === "CancelledError" + ) { + return null; + } + + return t("version.unable_to_load_release_notes"); + }, [releaseNotesQuery.error, t]); + + useEffect(() => { + if (!stableRelease || !serverVersion || disableNewReleaseCheck) { + setShouldNotify(false); + return; + } + + try { + const seenVersion = window.localStorage.getItem(LOCAL_STORAGE_KEY); + setShouldNotify(seenVersion !== serverVersion); + } catch (error) { + console.warn("Failed to read localStorage:", error); + setShouldNotify(true); + } + }, [serverVersion, stableRelease, disableNewReleaseCheck]); + + const markReleaseAsSeen = useCallback(() => { + if (!serverVersion) return; + try { + window.localStorage.setItem(LOCAL_STORAGE_KEY, serverVersion); + } catch (error) { + console.warn("Failed to write to localStorage:", error); + // Ignore failures, we still clear the notification for the session + } + setShouldNotify(false); + }, [serverVersion]); + + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + setOpen((prev) => { + if (prev && !nextOpen) { + markReleaseAsSeen(); + } + return nextOpen; + }); + }, + [markReleaseAsSeen], + ); + + if (!stableRelease || disableNewReleaseCheck) { + return ( + + {versionLabel} + + ); + } + + return ( + <> +
+ +
+ + + + + {t("version.whats_new_title", { version: displayVersion })} + + + {t("version.release_notes_description")} + + +
+ {isLoadingReleaseNotes ? ( +
+
+ ) : releaseNotesErrorMessage ? ( +
+
+ ) : releaseNotesQuery.data !== undefined ? ( + releaseNotesQuery.data.trim() ? ( + + {releaseNotesQuery.data} + + ) : ( +

+ {t("version.no_release_notes")} +

+ ) + ) : null} +
+
+ {t("version.release_notes_synced")} + +
+
+
+ + ); +} -- cgit v1.2.3-70-g09d2