mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 15:14: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 { GoosehintsModal } from './components/GoosehintsModal';
|
||||||
import { type ExtensionConfig } from './extensions';
|
import { type ExtensionConfig } from './extensions';
|
||||||
import { type Recipe } from './recipe';
|
import { type Recipe } from './recipe';
|
||||||
|
import AnnouncementModal from './components/AnnouncementModal';
|
||||||
|
|
||||||
import ChatView from './components/ChatView';
|
import ChatView from './components/ChatView';
|
||||||
import SuspenseLoader from './suspense-loader';
|
import SuspenseLoader from './suspense-loader';
|
||||||
@@ -610,6 +611,7 @@ export default function App() {
|
|||||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<AnnouncementModal />
|
||||||
</ModelAndProviderProvider>
|
</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,
|
actions,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
title: string;
|
title?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
actions: React.ReactNode; // Buttons for actions
|
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">
|
<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">
|
<div className="px-8 pb-0 space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex">
|
{title && (
|
||||||
<h2 className="text-2xl font-regular dark:text-white text-gray-900">{title}</h2>
|
<div className="flex">
|
||||||
</div>
|
<h2 className="text-2xl font-regular dark:text-white text-gray-900">{title}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{children && <div className="px-8">{children}</div>}
|
{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;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.md?raw' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
// Extend CSS properties to include Electron-specific properties
|
// Extend CSS properties to include Electron-specific properties
|
||||||
declare namespace React {
|
declare namespace React {
|
||||||
interface CSSProperties {
|
interface CSSProperties {
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export const UPDATES_ENABLED = true;
|
export const UPDATES_ENABLED = true;
|
||||||
export const COST_TRACKING_ENABLED = true;
|
export const COST_TRACKING_ENABLED = true;
|
||||||
|
export const ANNOUNCEMENTS_ENABLED = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user