fix: check if update is available based on auto-detected version channel.

This commit is contained in:
killian-larcher
2026-01-28 21:40:55 +01:00
parent 46022e8711
commit e8baa441cd
3 changed files with 100 additions and 49 deletions
@@ -7,10 +7,10 @@ import Link from "next/link";
export const UpdateNotification = () => { export const UpdateNotification = () => {
const {isUpdateAvailable, latestRelease, dismissUpdate} = useUpdateCheck(); const {isUpdateAvailable, newRelease, dismissUpdate} = useUpdateCheck();
const {state} = useSidebar(); const {state} = useSidebar();
if (!isUpdateAvailable || !latestRelease || state !== "expanded") { if (!isUpdateAvailable || !newRelease || state !== "expanded") {
return null; return null;
} }
@@ -29,21 +29,21 @@ export const UpdateNotification = () => {
<span className="sr-only">Dismiss</span> <span className="sr-only">Dismiss</span>
</button> </button>
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2">
<div <div
className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"> className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
<ArrowUpCircle className="size-4"/> <ArrowUpCircle className="size-4"/>
</div> </div>
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<span className="text-[12px] font-semibold leading-none">Update available</span> <span className="text-[10px] font-semibold leading-none">Update available</span>
<span <span
className="text-[10px] text-muted-foreground font-medium px-1.5 py-0.5 bg-primary/10 rounded-full"> className="text-[10px] text-muted-foreground font-medium px-1 py-0.5 bg-primary/10 rounded-full">
v{latestRelease.tag_name.replace(/^v/, "")} v{newRelease.tag_name.replace(/^v/, "")}
</span> </span>
</div> </div>
<Link <Link
href={latestRelease.html_url} href={newRelease.html_url}
target="_blank" target="_blank"
className="group inline-flex items-center gap-1 text-[11px] text-primary hover:underline font-medium mt-1" className="group inline-flex items-center gap-1 text-[11px] text-primary hover:underline font-medium mt-1"
> >
+33 -35
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import {useQuery} from "@tanstack/react-query"; import {useQuery} from "@tanstack/react-query";
import {getLatestRelease} from "../services/github"; import {getNewRelease} from "../services/github";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
@@ -9,53 +9,51 @@ const DISMISS_KEY = "portabase-update-dismissed";
const DISMISS_DURATION = 1000 * 60 * 60 * 24; const DISMISS_DURATION = 1000 * 60 * 60 * 24;
export const useUpdateCheck = () => { export const useUpdateCheck = () => {
const [isDismissed, setIsDismissed] = useState(true); const [isDismissed, setIsDismissed] = useState(true);
const currentVersion = env.NEXT_PUBLIC_PROJECT_VERSION!; const currentVersion = env.NEXT_PUBLIC_PROJECT_VERSION!;
const {data: latestRelease, isLoading} = useQuery({ const {data: newRelease, isLoading} = useQuery({
queryKey: ["latest-release"], queryKey: ["new-release"],
queryFn: () => getLatestRelease(currentVersion), queryFn: () => getNewRelease(currentVersion),
staleTime: 1000 * 60 * 60, staleTime: 1000 * 60 * 60,
}); });
console.log("newRelease", newRelease);
useEffect(() => { useEffect(() => {
if (!latestRelease) return; if (!newRelease) return;
const latestVersion = latestRelease.tag_name.replace(/^v/, "");
const cleanCurrentVersion = currentVersion?.replace(/^v/, "");
if (latestVersion && cleanCurrentVersion && latestVersion !== cleanCurrentVersion) {
const dismissedData = localStorage.getItem(DISMISS_KEY); const dismissedData = localStorage.getItem(DISMISS_KEY);
if (dismissedData) { if (dismissedData) {
const {version, timestamp} = JSON.parse(dismissedData); const {version, timestamp} = JSON.parse(dismissedData);
const now = Date.now(); const now = Date.now();
if (version === latestRelease.tag_name && now - timestamp < DISMISS_DURATION) { if (version === newRelease.name && now - timestamp < DISMISS_DURATION) {
setIsDismissed(true); setIsDismissed(true);
return; return;
} }
} }
setIsDismissed(false); setIsDismissed(false);
} else {
setIsDismissed(true);
}
}, [latestRelease, currentVersion]);
const dismissUpdate = () => { }, [newRelease])
if (latestRelease) {
localStorage.setItem(DISMISS_KEY, JSON.stringify({
version: latestRelease.tag_name,
timestamp: Date.now()
}));
setIsDismissed(true);
}
};
return {
latestRelease, const dismissUpdate = () => {
isLoading, if (newRelease) {
isUpdateAvailable: !isDismissed && latestRelease, localStorage.setItem(DISMISS_KEY, JSON.stringify({
dismissUpdate version: newRelease.name,
}; timestamp: Date.now()
}; }));
setIsDismissed(true);
}
};
return {
newRelease,
isLoading,
isUpdateAvailable: !isDismissed && newRelease,
dismissUpdate
};
}
;
+59 -6
View File
@@ -6,26 +6,79 @@ export interface GitHubRelease {
body: string; body: string;
} }
export const getLatestRelease = async (currentVersion: string): Promise<GitHubRelease | null> => { type ParsedVersion = {
major: number;
minor: number;
patch: number;
rc?: number;
};
function parseVersion(version: string): ParsedVersion {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
if (!match) return {
major: 0,
minor: 0,
patch: 0,
rc: undefined,
};
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
rc: match[4] ? Number(match[4]) : undefined,
};
}
export const getNewRelease = async (currentVersion: string): Promise<GitHubRelease | null> => {
try { try {
const response = await fetch("https://api.github.com/repos/Portabase/portabase/releases"); const response = await fetch("https://api.github.com/repos/Portabase/portabase/releases");
if (!response.ok) { if (!response.ok) {
return null; return null;
} }
const releases: GitHubRelease[] = await response.json(); const releases: GitHubRelease[] = await response.json();
const isStable = /^\d+\.\d+\.\d+$/.test(currentVersion); const isStable = /^\d+\.\d+\.\d+$/.test(currentVersion);
const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(currentVersion); const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(currentVersion);
if (isStable) { console.log("isStable", isStable)
return releases.find(r => !r.prerelease) || null; console.log("isRc", isRc)
}
const currentParsedVersion = parseVersion(currentVersion)
const latestStableVersion = releases.find(r => !r.prerelease)
const latestRcVersion = releases.find(r => r.name.includes("rc"))!
if (isRc) { if (isRc) {
return releases.find(r => r.tag_name.includes("rc") || !r.prerelease) || null; if (!latestRcVersion) return null
const latestRcParsedVersion = parseVersion(latestRcVersion.name)
if (currentParsedVersion.major < latestRcParsedVersion.major) return latestRcVersion;
if (currentParsedVersion.minor < latestRcParsedVersion.minor) return latestRcVersion;
if (currentParsedVersion.patch < latestRcParsedVersion.patch) return latestRcVersion;
if (currentParsedVersion.rc! < latestRcParsedVersion.rc!) return latestRcVersion;
if (!latestStableVersion) return null
const latestStableParsedVersion = parseVersion(latestStableVersion.name)
if (currentParsedVersion.major < latestStableParsedVersion.major) return latestStableVersion;
if (currentParsedVersion.minor < latestStableParsedVersion.minor) return latestStableVersion;
if (currentParsedVersion.patch < latestStableParsedVersion.patch) return latestStableVersion;
return null;
} else if (isStable) {
if (!latestStableVersion) return null
const latestStableParsedVersion = parseVersion(latestStableVersion.name)
if (currentParsedVersion.major < latestStableParsedVersion.major) return latestStableVersion;
if (currentParsedVersion.minor < latestStableParsedVersion.minor) return latestStableVersion;
if (currentParsedVersion.patch < latestStableParsedVersion.patch) return latestStableVersion;
return null;
} }
return releases[0] || null; return null;
} catch (error) { } catch (error) {
console.error("Failed to fetch latest release", error); console.error("Failed to fetch latest release", error);
return null; return null;