diff --git a/desktop/src/bridge/notifications.ts b/desktop/src/bridge/notifications.ts index 972ec015..152e6f53 100644 --- a/desktop/src/bridge/notifications.ts +++ b/desktop/src/bridge/notifications.ts @@ -28,8 +28,9 @@ class _Emitter { const emitter = new _Emitter(); -export async function addNotification(notification: MakeOptional): Promise { +export async function addNotification(notification: MakeOptional): Promise { _notifications.push({ + id: _notifications.length + 1, created_at: Math.floor(Date.now() / 1000), ...notification, }); @@ -42,7 +43,15 @@ export async function addNotification(notification: MakeOptional { +export async function removeNotificationById(id: number): Promise { + const index = _notifications.findIndex(noti => noti.id === id); + if (index > -1) { + _notifications.splice(index, 1); + emitter.emit('removed'); + } +} + +export async function removeNotificationByIndex(index: number): Promise { _notifications.splice(index, 1); emitter.emit('removed'); // await invoke('plugin:onelauncher|notifications_remove_by_index', { index }); diff --git a/desktop/src/types/core.d.ts b/desktop/src/types/core.d.ts index 13e178c2..64e06935 100644 --- a/desktop/src/types/core.d.ts +++ b/desktop/src/types/core.d.ts @@ -1,5 +1,6 @@ declare namespace Core { export interface Notification { + id: number; created_at: number; title: string; message: string; @@ -7,5 +8,5 @@ declare namespace Core { progress?: number; } - export type NotificationEvents = 'added' | 'removed' | 'cleared'; + export type NotificationEvents = 'init' | 'added' | 'removed' | 'cleared'; } \ No newline at end of file diff --git a/desktop/src/ui/App.tsx b/desktop/src/ui/App.tsx index 446bc1c6..4b47e1d5 100644 --- a/desktop/src/ui/App.tsx +++ b/desktop/src/ui/App.tsx @@ -5,6 +5,7 @@ import WindowFrame from './components/WindowFrame'; import Navbar from './components/Navbar'; import ErrorBoundary from './components/ErrorBoundary'; import AnimatedRoutes from './components/AnimatedRoutes'; +import NotificationOverlay from './components/overlay/notifications/NotificationOverlay'; function App(props: ParentProps) { if (!environment.isDev()) { @@ -31,6 +32,8 @@ function App(props: ParentProps) { + + ); } diff --git a/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx b/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx index 4400fdca..13cb0435 100644 --- a/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx +++ b/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx @@ -1,7 +1,8 @@ -import { type JSX, Show } from 'solid-js'; +import { type JSX, Show, createEffect, createSignal } from 'solid-js'; import { AlertTriangleIcon, CheckCircleIcon, FolderCheckIcon, FolderDownloadIcon, FolderXIcon, InfoCircleIcon, RefreshCcw02Icon } from '@untitled-theme/icons-solid'; import TimeAgo from '../../TimeAgo'; import { NotificationType } from '../../../../bridge/notifications'; +import { PausableTimer } from '~utils/PausableTimer'; interface NotificationComponentProps { overlay: boolean; @@ -48,11 +49,75 @@ function ColorFromNotificationType(type: NotificationType): string { } } +const TOTAL_SECONDS = 7.5; +// TODO(refactor): Use only an interval for this (Remove the pausable signal and in the interval, check if elapsed is longer than duraihon) function NotificationOverlayComponent(props: NotificationComponentProps) { + const [disappearing, setDisappearing] = createSignal(true); + const [pausable, setPausable] = createSignal(); + const [interval, setSecondsInterval] = createSignal(); + const [visible, setVisible] = createSignal(true); + const [secondsLeft, setSecondsLeft] = createSignal(TOTAL_SECONDS); + + createEffect(() => { + setDisappearing(props.data.progress === undefined || props.data.progress > 1); + + if (disappearing()) { + initTimers(); + } + else if (pausable()) { + clearTimeout(pausable()?.timeout); + setPausable(undefined); + } + }); + + function initTimers() { + setPausable(new PausableTimer(hide, (TOTAL_SECONDS + 0.5) * 1000)); + setSecondsInterval(new PausableTimer(onInterval, 1000, true)); + } + + function onInterval() { + setSecondsLeft(secondsLeft() - 1); + } + + function hide() { + setVisible(false); + interval()?.stop(); + setSecondsInterval(undefined); + } + + function onEnter() { + pausable()?.pause(); + interval()?.pause(); + } + + function onLeave() { + pausable()?.resume(); + interval()?.resume(); + } + return ( -
- {props.data.title} -
+ +
onEnter()} + onMouseLeave={() => onLeave()} + class="flex flex-col overflow-hidden rounded-lg bg-component-bg" + > +
+ +
+ + +
+
+
+ +
+
); } @@ -69,12 +134,14 @@ function NotificationPopupComponent(props: NotificationComponentProps) { {props.data.message}
-
- - - - -
+ +
+ + + + +
+
diff --git a/desktop/src/ui/components/overlay/notifications/NotificationOverlay.tsx b/desktop/src/ui/components/overlay/notifications/NotificationOverlay.tsx index 8f1076cb..b6b50a8c 100644 --- a/desktop/src/ui/components/overlay/notifications/NotificationOverlay.tsx +++ b/desktop/src/ui/components/overlay/notifications/NotificationOverlay.tsx @@ -1,3 +1,59 @@ -export default function NotificationOverlay() { +import { For, createSignal } from 'solid-js'; +import Popup from '../Popup'; +import NotificationComponent from './NotificationComponent'; +import useNotifications from '~ui/hooks/useNotifications'; +import createPausableTimer from '~utils/PausableTimer'; +export default function NotificationOverlay() { + const [notifications] = useNotifications(); + + // function onAdded() { + // const noti = notifications()[notifications().length - 1]; + // if (noti) { + // displayed().push(noti); + // setDisplayed([...displayed()]); + // const id = noti.id; + + // const pausable = createPausableTimer(() => { + // if (!notifications().includes(noti)) + // return; + + // const index = displayed().findIndex(noti => noti.id === id); + + // if (index > -1) { + // displayed().splice(index, 1); + // setDisplayed([...displayed()]); + // } + // }, 2500); + // } + // } + + // function onRemoved() { + // const index = displayed().findIndex(noti => !notifications().includes(noti)); + // if (index > -1) { + // displayed().splice(index, 1); + // setDisplayed([...displayed()]); + // } + // } + + // function onClear() { + // setDisplayed([]); + // } + + return ( + {}} + visible={() => true} + mount={document.body} + ref={ref => ref.classList.add('fixed', 'bottom-8', 'right-8')} + > +
+ + {notification => ( + + )} + +
+
+ ); } diff --git a/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx b/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx index b90e9286..3062726e 100644 --- a/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx +++ b/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx @@ -1,13 +1,14 @@ -import { For, Match, Switch, createEffect, createSignal, onMount } from 'solid-js'; +import { For, Match, Switch, onMount } from 'solid-js'; import { Settings01Icon, Trash01Icon } from '@untitled-theme/icons-solid'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; import Popup from '../Popup'; import Button from '../../base/Button'; import * as manager from '../../../../bridge/notifications'; import NotificationComponent from './NotificationComponent'; +import useNotifications from '~ui/hooks/useNotifications'; function NotificationPopup(props: Popup.PopupProps) { - const [notifications, setNotifications] = createSignal([]); + const [notifications] = useNotifications(updateSize); let inner!: HTMLDivElement; let parent!: HTMLDivElement; @@ -19,12 +20,6 @@ function NotificationPopup(props: Popup.PopupProps) { } } - async function refetchNotifications() { - const notis = await manager.getNotifications(); - setNotifications(notis); - updateSize(); - } - onMount(() => { document.addEventListener('keypress', (e) => { if (e.key === 'n') { @@ -32,40 +27,29 @@ function NotificationPopup(props: Popup.PopupProps) { title: 'Test Notification', message: 'This is a test notification', notification_type: manager.NotificationType.Download, - progress: 0.39, + ...(Math.random() > 0.7 ? { progress: 0.39 } : {}), }); } }); - - refetchNotifications(); - manager.on('added', refetchNotifications); - manager.on('removed', refetchNotifications); - manager.on('cleared', refetchNotifications); }); return (
-
+

Notifications

- - {notifications().length} - 0}>
- {(noti) => { - console.log(noti); - return ( -
- - -
- ); - }} + {noti => ( +
+ + +
+ )}
diff --git a/desktop/src/ui/hooks/useNotifications.ts b/desktop/src/ui/hooks/useNotifications.ts new file mode 100644 index 00000000..6bfe1efe --- /dev/null +++ b/desktop/src/ui/hooks/useNotifications.ts @@ -0,0 +1,29 @@ +import { type Signal, createSignal, onMount } from 'solid-js'; +import * as manager from '~bridge/notifications'; + +type Notifications = Core.Notification[]; +type OnUpdateFn = (event: Core.NotificationEvents) => unknown; + +function useNotifications(onUpdate?: OnUpdateFn): Signal { + const [notifications, setNotifications] = createSignal([]); + + async function update(event: Core.NotificationEvents) { + const notis = await manager.getNotifications(); + setNotifications(notis); + + if (onUpdate) + onUpdate(event); + } + + onMount(() => { + update('init'); + + manager.on('added', () => update('added')); + manager.on('removed', () => update('removed')); + manager.on('cleared', () => update('cleared')); + }); + + return [notifications, setNotifications]; +} + +export default useNotifications; diff --git a/desktop/src/utils/PausableTimer.ts b/desktop/src/utils/PausableTimer.ts new file mode 100644 index 00000000..0bef3639 --- /dev/null +++ b/desktop/src/utils/PausableTimer.ts @@ -0,0 +1,59 @@ +export class PausableTimer { + private _timeout: NodeJS.Timeout; + private _paused: boolean = false; + private _unixStarted: number = 0; + private _unixStopped: number = 0; + + constructor( + private readonly callback: () => unknown, + private readonly duration: number, + private readonly interval: boolean = false, + ) { + this._timeout = this._createTimeout(); + } + + private _createTimeout(duration: number = this.duration): NodeJS.Timeout { + this._unixStarted = Date.now(); + if (this.interval) + return setInterval(this.callback, duration); + else + return setTimeout(this.callback, duration); + } + + private _clearTimeout() { + this._unixStopped = Date.now(); + if (this.interval) + clearInterval(this._timeout); + else + clearTimeout(this._timeout); + } + + public resume(restart: boolean = false) { + const elapsed = this._unixStopped - this._unixStarted; + const duration = this.interval || restart ? this.duration : this.duration - elapsed; + + this._paused = false; + this._timeout = this._createTimeout(duration); + } + + public pause() { + this._clearTimeout(); + this._paused = true; + } + + public stop() { + this._clearTimeout(); + } + + public get timeout() { + return this._timeout; + } + + public get paused() { + return this._paused; + } +} + +export default function createPausableTimer(callback: () => unknown, duration: number): PausableTimer { + return new PausableTimer(callback, duration); +}