Added announcement modal (#3098)

This commit is contained in:
Zane
2025-06-27 10:50:32 -07:00
committed by GitHub
parent dfc9dd2bf8
commit e40e84ec2e
7 changed files with 178 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
// Map of announcement file names to their content
export const announcementContents: Record<string, string> = {};
// Helper function to get announcement content by filename
export function getAnnouncementContent(filename: string): string | null {
return announcementContents[filename] || null;
}

View File

@@ -0,0 +1 @@
[]

View File

@@ -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}
/>
)}
<AnnouncementModal />
</ModelAndProviderProvider>
);
}

View File

@@ -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<string | null>(
null
);
const [unseenAnnouncements, setUnseenAnnouncements] = useState<AnnouncementMeta[]>([]);
// 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 (
<BaseModal
isOpen={showAnnouncementModal}
title={
unseenAnnouncements.length === 1
? unseenAnnouncements[0].title
: `${unseenAnnouncements.length}`
}
actions={
<div className="flex justify-end pb-4">
<Button
variant="ghost"
onClick={handleCloseAnnouncement}
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-textProminent font-medium text-md"
>
Got it!
</Button>
</div>
}
>
<div className="max-h-96 overflow-y-auto -mx-12">
<div className="px-4 py-10">
<MarkdownContent content={combinedAnnouncementContent} />
</div>
</div>
</BaseModal>
);
}

View File

@@ -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({
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden p-[16px] pt-[24px] pb-0">
<div className="px-8 pb-0 space-y-8">
{/* Header */}
<div className="flex">
<h2 className="text-2xl font-regular dark:text-white text-gray-900">{title}</h2>
</div>
{title && (
<div className="flex">
<h2 className="text-2xl font-regular dark:text-white text-gray-900">{title}</h2>
</div>
)}
{/* Content */}
{children && <div className="px-8">{children}</div>}

View File

@@ -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 {

View File

@@ -1,2 +1,3 @@
export const UPDATES_ENABLED = true;
export const COST_TRACKING_ENABLED = true;
export const ANNOUNCEMENTS_ENABLED = false;