mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 14:44:21 +01:00
Added announcement modal (#3098)
This commit is contained in:
7
ui/desktop/announcements/content.ts
Normal file
7
ui/desktop/announcements/content.ts
Normal 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;
|
||||
}
|
||||
1
ui/desktop/announcements/index.json
Normal file
1
ui/desktop/announcements/index.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
156
ui/desktop/src/components/AnnouncementModal.tsx
Normal file
156
ui/desktop/src/components/AnnouncementModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>}
|
||||
|
||||
5
ui/desktop/src/json.d.ts
vendored
5
ui/desktop/src/json.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export const UPDATES_ENABLED = true;
|
||||
export const COST_TRACKING_ENABLED = true;
|
||||
export const ANNOUNCEMENTS_ENABLED = false;
|
||||
|
||||
Reference in New Issue
Block a user