import { useState, useEffect } from 'react'; import { BaseModal } from './ui/BaseModal'; import MarkdownContent from './MarkdownContent'; import { ANNOUNCEMENTS_ENABLED } from '../updates'; import packageJson from '../../package.json'; import { getAnnouncementContent } from '../../announcements/content'; import { Button } from './ui/button'; interface AnnouncementMeta { id: string; version: string; title: string; file: string; } // Simple version comparison function for semantic versioning (x.y.z) // Returns: -1 if a < b, 0 if a === b, 1 if a > b function compareVersions(a: string, b: string): number { const parseVersion = (version: string) => version.split('.').map((part) => parseInt(part, 10)); const versionA = parseVersion(a); const versionB = parseVersion(b); for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) { const partA = versionA[i] || 0; const partB = versionB[i] || 0; if (partA < partB) return -1; if (partA > partB) return 1; } return 0; } export default function AnnouncementModal() { const [showAnnouncementModal, setShowAnnouncementModal] = useState(false); const [combinedAnnouncementContent, setCombinedAnnouncementContent] = useState( null ); const [unseenAnnouncements, setUnseenAnnouncements] = useState([]); // Load announcements and check for unseen ones useEffect(() => { const loadAnnouncements = async () => { // Only proceed if announcements are enabled if (!ANNOUNCEMENTS_ENABLED) { return; } try { // Load the announcements index const indexModule = await import('../../announcements/index.json'); const announcements = indexModule.default as AnnouncementMeta[]; // Get current app version const currentVersion = packageJson.version; // Filter announcements to only include those for current version or earlier const applicableAnnouncements = announcements.filter((announcement) => { // Simple version comparison - assumes semantic versioning const announcementVersion = announcement.version; return compareVersions(announcementVersion, currentVersion) <= 0; }); // Get list of seen announcement IDs const seenAnnouncementIds = JSON.parse( localStorage.getItem('seenAnnouncementIds') || '[]' ) as string[]; // Find ALL unseen announcements (in order) const unseenAnnouncementsList = applicableAnnouncements.filter( (announcement) => !seenAnnouncementIds.includes(announcement.id) ); if (unseenAnnouncementsList.length > 0) { // Load content for all unseen announcements const contentPromises = unseenAnnouncementsList.map(async (announcement) => { const content = getAnnouncementContent(announcement.file); return { announcement, content }; }); const loadedAnnouncements = await Promise.all(contentPromises); const validAnnouncements = loadedAnnouncements.filter(({ content }) => content); if (validAnnouncements.length > 0) { // Combine all announcement content with separators const combinedContent = validAnnouncements .map(({ content }) => content) .join('\n\n---\n\n'); setUnseenAnnouncements(validAnnouncements.map(({ announcement }) => announcement)); setCombinedAnnouncementContent(combinedContent); setShowAnnouncementModal(true); } } } catch (error) { console.log('No announcements found or failed to load:', error); } }; loadAnnouncements(); }, []); const handleCloseAnnouncement = () => { if (unseenAnnouncements.length === 0) return; // Get existing seen announcement IDs const seenAnnouncementIds = JSON.parse( localStorage.getItem('seenAnnouncementIds') || '[]' ) as string[]; // Add all unseen announcement IDs to the seen list const newSeenIds = [...seenAnnouncementIds]; unseenAnnouncements.forEach((announcement) => { if (!newSeenIds.includes(announcement.id)) { newSeenIds.push(announcement.id); } }); localStorage.setItem('seenAnnouncementIds', JSON.stringify(newSeenIds)); setShowAnnouncementModal(false); }; // Don't render anything if there are no announcements to show if (!combinedAnnouncementContent || unseenAnnouncements.length === 0) { return null; } return ( } >
); }