Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add announcements system #703

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
123 changes: 123 additions & 0 deletions frontend/src/components/AnnouncementsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { DialogButton, Focusable, PanelSection } from '@decky/ui';
import { useEffect, useMemo, useState } from 'react';
import { FaTimes } from 'react-icons/fa';

import { Announcement, getAnnouncements } from '../store';
import { useSetting } from '../utils/hooks/useSetting';

const SEVERITIES = {
High: {
color: '#bb1414',
text: '#fff',
},
Medium: {
color: '#bbbb14',
text: '#fff',
},
Low: {
color: '#1488bb',
text: '#fff',
},
};

const welcomeAnnouncement: Announcement = {
id: 'welcomeAnnouncement',
title: 'Welcome to Decky!',
text: 'We hope you enjoy using Decky! If you have any questions or feedback, please let us know.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should people direct feedback?

(Also i assume this shows the first time you use decky? Might want to add something like start with looking at the store)

Copy link
Contributor

@RodoMa92 RodoMa92 Sep 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, but there is another blocking issue. The text need to be moved in en-US.json and not hardcoded here.

created: Date.now().toString(),
updated: Date.now().toString(),
};

export function AnnouncementsDisplay() {
const [announcements, setAnnouncements] = useState<Announcement[]>([welcomeAnnouncement]);
// showWelcome will display a welcome motd, the welcome motd has an id of "welcome" and once that is saved to hiddenMotdId, it will not show again
const [hiddenAnnouncementIds, setHiddenAnnouncementIds] = useSetting<string[]>('hiddenAnnouncementIds', []);

function addAnnouncements(newAnnouncements: Announcement[]) {
// Removes any duplicates and sorts by created date
setAnnouncements((oldAnnouncements) => {
const newArr = [...oldAnnouncements, ...newAnnouncements];
const setOfIds = new Set(newArr.map((a) => a.id));
return (
(
Array.from(setOfIds)
.map((id) => newArr.find((a) => a.id === id))
// Typescript doesn't type filter(Boolean) correctly, so I have to assert this
.filter(Boolean) as Announcement[]
).sort((a, b) => {
return new Date(b.created).getTime() - new Date(a.created).getTime();
})
);
});
}

async function fetchAnnouncement() {
const announcements = await getAnnouncements();
announcements && addAnnouncements(announcements);
}

useEffect(() => {
void fetchAnnouncement();
}, []);

const currentlyDisplayingAnnouncement: Announcement | null = useMemo(() => {
return announcements.find((announcement) => !hiddenAnnouncementIds.includes(announcement.id)) || null;
}, [announcements, hiddenAnnouncementIds]);

function hideAnnouncement(id: string) {
setHiddenAnnouncementIds([...hiddenAnnouncementIds, id]);
void fetchAnnouncement();
}

if (!currentlyDisplayingAnnouncement) {
return null;
}

// Severity is not implemented in the API currently
const severity = SEVERITIES['Low'];

return (
<PanelSection>
<Focusable
style={{
// Transparency is 20% of the color
backgroundColor: `${severity.color}33`,
color: severity.text,
borderColor: severity.color,
borderWidth: '2px',
borderStyle: 'solid',
padding: '0.7rem',
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontWeight: 'bold' }}>{currentlyDisplayingAnnouncement.title}</span>
<DialogButton
style={{
width: '1rem',
minWidth: '1rem',
height: '1rem',
padding: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: '.75rem',
right: '.75rem',
}}
onClick={() => hideAnnouncement(currentlyDisplayingAnnouncement.id)}
>
<FaTimes
style={{
height: '.75rem',
}}
/>
</DialogButton>
</div>
<span style={{ fontSize: '0.75rem', whiteSpace: 'pre-line' }}>{currentlyDisplayingAnnouncement.text}</span>
</Focusable>
</PanelSection>
);
}
2 changes: 2 additions & 0 deletions frontend/src/components/PluginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';

import { Plugin } from '../plugin';
import { AnnouncementsDisplay } from './AnnouncementsDisplay';
import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
Expand Down Expand Up @@ -42,6 +43,7 @@ const PluginView: FC = () => {
paddingTop: '16px',
}}
>
<AnnouncementsDisplay />
<PanelSection>
{pluginList
.filter((p) => p.content)
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/store.tsx → frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,62 @@ export interface PluginInstallRequest {
installType: InstallType;
}

export interface Announcement {
id: string;
title: string;
text: string;
created: string;
updated: string;
}

// name: version
export type PluginUpdateMapping = Map<string, StorePluginVersion>;

export async function getStore(): Promise<Store> {
return await getSetting<Store>('store', Store.Default);
}

export async function getAnnouncements(): Promise<Announcement[]> {
let version = await window.DeckyPluginLoader.updateVersion();
let store = await getSetting<Store | null>('store', null);
let customURL = await getSetting<string>(
'announcements-url',
'https://plugins.deckbrew.xyz/v1/announcements/-/current',
);

if (store === null) {
console.log('Could not get store, using Default.');
await setSetting('store', Store.Default);
store = Store.Default;
}

let resolvedURL;
switch (store) {
case Store.Default:
resolvedURL = 'https://plugins.deckbrew.xyz/v1/announcements/-/current';
break;
case Store.Testing:
resolvedURL = 'https://testing.deckbrew.xyz/v1/announcements/-/current';
break;
case Store.Custom:
resolvedURL = customURL;
break;
default:
console.error('Somehow you ended up without a standard URL, using the default URL.');
resolvedURL = 'https://plugins.deckbrew.xyz/v1/announcements/-/current';
break;
}
const res = await fetch(resolvedURL, {
method: 'GET',
headers: {
'X-Decky-Version': version.current,
},
});
if (res.status !== 200) return [];
const json = await res.json();
return json ?? [];
}

export async function getPluginList(
sort_by: SortOptions | null = null,
sort_direction: SortDirections | null = null,
Expand Down
Loading