From e40e84ec2ecf58c6be2efbc233fb2a44ef5a41b6 Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:50:32 -0700 Subject: [PATCH] Added announcement modal (#3098) --- ui/desktop/announcements/content.ts | 7 + ui/desktop/announcements/index.json | 1 + ui/desktop/src/App.tsx | 2 + .../src/components/AnnouncementModal.tsx | 156 ++++++++++++++++++ ui/desktop/src/components/ui/BaseModal.tsx | 10 +- ui/desktop/src/json.d.ts | 5 + ui/desktop/src/updates.ts | 1 + 7 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 ui/desktop/announcements/content.ts create mode 100644 ui/desktop/announcements/index.json create mode 100644 ui/desktop/src/components/AnnouncementModal.tsx diff --git a/ui/desktop/announcements/content.ts b/ui/desktop/announcements/content.ts new file mode 100644 index 00000000..4b20e486 --- /dev/null +++ b/ui/desktop/announcements/content.ts @@ -0,0 +1,7 @@ +// Map of announcement file names to their content +export const announcementContents: Record = {}; + +// Helper function to get announcement content by filename +export function getAnnouncementContent(filename: string): string | null { + return announcementContents[filename] || null; +} diff --git a/ui/desktop/announcements/index.json b/ui/desktop/announcements/index.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/ui/desktop/announcements/index.json @@ -0,0 +1 @@ +[] diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index fe0a055a..14c72ac5 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -12,6 +12,7 @@ import { extractExtensionName } from './components/settings/extensions/utils'; import { GoosehintsModal } from './components/GoosehintsModal'; import { type ExtensionConfig } from './extensions'; import { type Recipe } from './recipe'; +import AnnouncementModal from './components/AnnouncementModal'; import ChatView from './components/ChatView'; import SuspenseLoader from './suspense-loader'; @@ -610,6 +611,7 @@ export default function App() { setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} /> )} + ); } diff --git a/ui/desktop/src/components/AnnouncementModal.tsx b/ui/desktop/src/components/AnnouncementModal.tsx new file mode 100644 index 00000000..9a74462a --- /dev/null +++ b/ui/desktop/src/components/AnnouncementModal.tsx @@ -0,0 +1,156 @@ +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 ( + + + + } + > +
+
+ +
+
+
+ ); +} diff --git a/ui/desktop/src/components/ui/BaseModal.tsx b/ui/desktop/src/components/ui/BaseModal.tsx index 1550c822..a518e5d1 100644 --- a/ui/desktop/src/components/ui/BaseModal.tsx +++ b/ui/desktop/src/components/ui/BaseModal.tsx @@ -8,7 +8,7 @@ export function BaseModal({ actions, }: { isOpen: boolean; - title: string; + title?: string; children: React.ReactNode; actions: React.ReactNode; // Buttons for actions }) { @@ -19,9 +19,11 @@ export function BaseModal({
{/* Header */} -
-

{title}

-
+ {title && ( +
+

{title}

+
+ )} {/* Content */} {children &&
{children}
} diff --git a/ui/desktop/src/json.d.ts b/ui/desktop/src/json.d.ts index 5bb9ca45..02b4714e 100644 --- a/ui/desktop/src/json.d.ts +++ b/ui/desktop/src/json.d.ts @@ -38,6 +38,11 @@ declare module '*.mp4' { export default value; } +declare module '*.md?raw' { + const value: string; + export default value; +} + // Extend CSS properties to include Electron-specific properties declare namespace React { interface CSSProperties { diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts index 3029a850..3ec4002a 100644 --- a/ui/desktop/src/updates.ts +++ b/ui/desktop/src/updates.ts @@ -1,2 +1,3 @@ export const UPDATES_ENABLED = true; export const COST_TRACKING_ENABLED = true; +export const ANNOUNCEMENTS_ENABLED = false;