import { closeAllModals, openModal } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; import DOMPurify from 'dompurify'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import packageJson from '../../package.json'; import { formatHrDateTime } from '/@/renderer/utils/format'; import { Button } from '/@/shared/components/button/button'; import { Center } from '/@/shared/components/center/center'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { Select } from '/@/shared/components/select/select'; import { Spinner } from '/@/shared/components/spinner/spinner'; import { Stack } from '/@/shared/components/stack/stack'; import { Text } from '/@/shared/components/text/text'; import { useLocalStorage } from '/@/shared/hooks/use-local-storage'; const GITHUB_RELEASES_URL = 'https://api.github.com/repos/bouliehaan/samo/releases'; const GITHUB_COMPARE_URL = 'https://api.github.com/repos/bouliehaan/samo/compare'; const RELEASES_TO_FETCH = 30; interface GitHubCompareCommit { commit: { author: { date: string; name: string }; message: string; }; html_url: string; sha: string; } interface GitHubCompareResponse { commits: GitHubCompareCommit[]; total_commits: number; } interface GitHubRelease { body: null | string; name: null | string; prerelease: boolean; published_at: string; tag_name: string; } interface ReleaseNotesContentProps { onDismiss: () => void; version: string; } function isAlphaVersion(version: string): boolean { return version.includes('-alpha'); } function parseVersionFromTag(tagName: string): string { return tagName.startsWith('v') ? tagName.slice(1) : tagName; } function toTag(version: string): string { return version.startsWith('v') ? version : `v${version}`; } const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) => { const { t } = useTranslation(); const [selectedVersion, setSelectedVersion] = useState(version); const isAlpha = isAlphaVersion(selectedVersion); // Fetch list of recent releases for the selector const { data: releasesList = [] } = useQuery({ queryFn: async () => { const response = await fetch(`${GITHUB_RELEASES_URL}?per_page=${RELEASES_TO_FETCH}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data: GitHubRelease[] = await response.json(); return data; }, queryKey: ['github-releases-list'], retry: 2, }); const latestStableRelease = useMemo(() => { return releasesList.find((r) => !r.prerelease); }, [releasesList]); const releaseOptions = useMemo(() => { const options = releasesList.slice(0, RELEASES_TO_FETCH).map((r) => { const v = parseVersionFromTag(r.tag_name); const dateStr = formatHrDateTime(r.published_at); return { label: dateStr ? `${v} - ${dateStr}` : v, value: v, }; }); const versions = options.map((o) => o.value); if (!versions.includes(version)) { options.unshift({ label: version, value: version }); } return options; }, [releasesList, version]); // For alpha: fetch commits between latest stable and development branch const { data: compareData, isError: isCompareError, isLoading: isCompareLoading, } = useQuery({ enabled: isAlpha && !!latestStableRelease, queryFn: async () => { const base = latestStableRelease!.tag_name; const head = 'development'; const response = await fetch(`${GITHUB_COMPARE_URL}/${base}...${head}?per_page=100`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data: GitHubCompareResponse = await response.json(); return data; }, queryKey: ['github-compare', latestStableRelease?.tag_name, 'development'], retry: 2, }); // For non-alpha: fetch release by tag const { data: releaseData, isError, isLoading, } = useQuery({ enabled: !isAlpha, queryFn: async () => { const response = await fetch(`${GITHUB_RELEASES_URL}/tags/${toTag(selectedVersion)}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data: GitHubRelease = await response.json(); return data; }, queryKey: ['github-release', selectedVersion], retry: 2, }); // Convert markdown to HTML using GitHub's markdown API const { data: htmlContent, isLoading: isConverting } = useQuery({ enabled: !isAlpha && !!releaseData?.body, queryFn: async () => { const response = await fetch('https://api.github.com/markdown', { body: JSON.stringify({ mode: 'gfm', text: releaseData?.body ?? '', }), headers: { 'Content-Type': 'application/json', }, method: 'POST', }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return response.text(); }, queryKey: ['github-markdown', releaseData?.body], retry: 2, }); const sanitizedHtml = useMemo(() => { if (!htmlContent) return ''; return DOMPurify.sanitize(htmlContent, { ALLOWED_ATTR: ['alt', 'href', 'src', 'title'], ALLOWED_TAGS: [ 'a', 'blockquote', 'br', 'code', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'li', 'ol', 'p', 'pre', 'strong', 'u', 'ul', ], }); }, [htmlContent]); const isLoadingState = isAlpha ? isCompareLoading : isLoading || isConverting; const isErrorState = isAlpha ? isCompareError : isError || !releaseData; if (isLoadingState) { return (