From 8e5d7e77fc55cf70e5c344e4e8c26c50ed9c84f2 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Fri, 10 May 2024 19:52:27 +0200 Subject: [PATCH] merge(notifications): Merged notification work --- desktop/package.json | 1 + desktop/src-tauri/tauri.conf.json | 4 +- desktop/src/bridge/notifications.ts | 46 ++++++++ desktop/src/constants.ts | 1 + desktop/src/types/core.d.ts | 11 ++ desktop/src/types/global.d.ts | 1 + desktop/src/ui/App.tsx | 19 ++-- desktop/src/ui/components/ErrorBoundary.tsx | 21 ++++ desktop/src/ui/components/Navbar.tsx | 58 +++++++--- desktop/src/ui/components/TimeAgo.tsx | 48 ++++++++ .../components/{native => }/WindowFrame.tsx | 8 +- .../src/ui/components/base/Button.module.scss | 11 +- desktop/src/ui/components/base/Button.tsx | 9 +- .../ui/components/overlay/AccountPopup.tsx | 8 +- desktop/src/ui/components/overlay/Popup.tsx | 25 ++++- .../notifications/NotificationComponent.tsx | 102 +++++++++++++++++ .../notifications/NotificationPopup.tsx | 91 +++++++++++++++ desktop/src/ui/pages/Home.tsx | 6 +- desktop/uno.config.ts | 3 + pnpm-lock.yaml | 106 ++++++++++++++++++ 20 files changed, 533 insertions(+), 46 deletions(-) create mode 100644 desktop/src/bridge/notifications.ts create mode 100644 desktop/src/constants.ts create mode 100644 desktop/src/types/core.d.ts create mode 100644 desktop/src/ui/components/ErrorBoundary.tsx create mode 100644 desktop/src/ui/components/TimeAgo.tsx rename desktop/src/ui/components/{native => }/WindowFrame.tsx (87%) create mode 100644 desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx create mode 100644 desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx diff --git a/desktop/package.json b/desktop/package.json index 7cce7fa1..324315c9 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -20,6 +20,7 @@ "dependencies": { "@fontsource/poppins": "^5.0.14", "@iconify-json/logos": "^1.1.42", + "@solid-primitives/refs": "^1.0.8", "@solidjs/router": "^0.13.3", "@tauri-apps/api": "^2.0.0-beta", "@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2", diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 5839c55d..f26a0915 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -44,8 +44,8 @@ "hiddenTitle": true, "width": 1280, "height": 802, - "minWidth": 600, - "minHeight": 400, + "minWidth": 1020, + "minHeight": 600, "resizable": true, "center": true, "decorations": false, diff --git a/desktop/src/bridge/notifications.ts b/desktop/src/bridge/notifications.ts new file mode 100644 index 00000000..4f2e0229 --- /dev/null +++ b/desktop/src/bridge/notifications.ts @@ -0,0 +1,46 @@ +import { invoke } from '@tauri-apps/api/core'; +import type { UnlistenFn } from '@tauri-apps/api/event'; +import { listen } from '@tauri-apps/api/event'; + +export enum NotificationType { + Info = 'Info', + Alert = 'Alert', + Success = 'Success', + Download = 'Download', + DownloadSuccess = 'DownloadSuccess', + DownloadError = 'DownloadError', + Refresh = 'Refresh', +} + +export async function addNotification(notification: MakeOptional): Promise { + await invoke('plugin:onelauncher|notifications_add', { + notification: { + created_at: Math.floor(Date.now() / 1000), + ...notification, + }, + }); +} + +export async function removeNotification(index: number): Promise { + await invoke('plugin:onelauncher|notifications_remove_by_index', { index }); +} + +export async function clearNotifications(): Promise { + await invoke('plugin:onelauncher|notifications_clear'); +} + +export async function getNotifications(): Promise { + return await invoke('plugin:onelauncher|notifications_get'); +} + +export async function on< + T extends Core.NotificationEvents, +>( + event: T, + callback: () => void, +): Promise { + return await listen(`notifications_event`, (e) => { + if (e.payload.toLowerCase() === event) + callback(); + }); +}; diff --git a/desktop/src/constants.ts b/desktop/src/constants.ts new file mode 100644 index 00000000..8173384c --- /dev/null +++ b/desktop/src/constants.ts @@ -0,0 +1 @@ +export const WEBSITE = 'https://polyfrost.org/'; diff --git a/desktop/src/types/core.d.ts b/desktop/src/types/core.d.ts new file mode 100644 index 00000000..13e178c2 --- /dev/null +++ b/desktop/src/types/core.d.ts @@ -0,0 +1,11 @@ +declare namespace Core { + export interface Notification { + created_at: number; + title: string; + message: string; + notification_type: NotificationType; + progress?: number; + } + + export type NotificationEvents = 'added' | 'removed' | 'cleared'; +} \ No newline at end of file diff --git a/desktop/src/types/global.d.ts b/desktop/src/types/global.d.ts index 5fbb961c..441a7929 100644 --- a/desktop/src/types/global.d.ts +++ b/desktop/src/types/global.d.ts @@ -1,3 +1,4 @@ declare namespace globalThis { export type WithIndex = T & { index: number } + export type MakeOptional = Omit & Partial> } \ No newline at end of file diff --git a/desktop/src/ui/App.tsx b/desktop/src/ui/App.tsx index 5e1446e5..3fe479f8 100644 --- a/desktop/src/ui/App.tsx +++ b/desktop/src/ui/App.tsx @@ -2,8 +2,9 @@ import type { ParentProps } from 'solid-js'; import { Transition } from 'solid-transition-group'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid'; import environment from '../utils/environment'; -import WindowFrame from './components/native/WindowFrame'; +import WindowFrame from './components/WindowFrame'; import Navbar from './components/Navbar'; +import ErrorBoundary from './components/ErrorBoundary'; function App(props: ParentProps) { if (!environment.isDev()) { @@ -14,21 +15,21 @@ function App(props: ParentProps) { return (
-
- {/* This was a pain to do */}
- - - {props.children} - -
- + + + + {props.children} + +
+ +
diff --git a/desktop/src/ui/components/ErrorBoundary.tsx b/desktop/src/ui/components/ErrorBoundary.tsx new file mode 100644 index 00000000..3096909c --- /dev/null +++ b/desktop/src/ui/components/ErrorBoundary.tsx @@ -0,0 +1,21 @@ +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary as Capturer } from 'solid-js'; + +// TODO: Uncomment code +function ErrorBoundary(props: ParentProps) { + return ( + <>{props.children} + // ( + //
+ //

An unexpected error has occurred

+ //

{err.toString()}

+ //
+ // )} + // > + // {props.children} + //
+ ); +} + +export default ErrorBoundary; diff --git a/desktop/src/ui/components/Navbar.tsx b/desktop/src/ui/components/Navbar.tsx index 61b9c422..e508a446 100644 --- a/desktop/src/ui/components/Navbar.tsx +++ b/desktop/src/ui/components/Navbar.tsx @@ -1,11 +1,15 @@ import { A } from '@solidjs/router'; import { createSignal } from 'solid-js'; import { Bell01Icon, Cloud01Icon, Settings01Icon, TerminalBrowserIcon } from '@untitled-theme/icons-solid'; +import { open } from '@tauri-apps/plugin-shell'; import useAccount from '../hooks/useAccount'; import PolyfrostFull from './logos/PolyfrostFull'; import AccountPopup from './overlay/AccountPopup'; import PlayerHead from './game/PlayerHead'; import Button from './base/Button'; +import NotificationPopup from './overlay/notifications/NotificationPopup'; +import Popup from './overlay/Popup'; +import { WEBSITE } from '~constants'; interface NavbarLinkProps { path: string; @@ -28,38 +32,54 @@ function NavbarLink(props: NavbarLinkProps) { function Navbar() { const [profileMenuOpen, setProfileMenuOpen] = createSignal(false); + const [notificationMenuOpen, setNotificationMenuOpen] = createSignal(false); + const account = useAccount(); let profileButton!: HTMLButtonElement; + let notificationButton!: HTMLButtonElement; return ( -
+
- +
open(WEBSITE)} class="flex items-center justify-center active:scale-90 transition-transform w-min"> + +
-
+
- - - - -
@@ -67,8 +87,18 @@ function Navbar() { { + el.style.left = `${profileButton.getBoundingClientRect().right - el.getBoundingClientRect().width}px`; + el.style.top = `${profileButton.getBoundingClientRect().bottom}px`; + }} + class="mt-2" + /> + + Popup.setPos(notificationButton, el)} + class="mt-2" />
); diff --git a/desktop/src/ui/components/TimeAgo.tsx b/desktop/src/ui/components/TimeAgo.tsx new file mode 100644 index 00000000..e18b105f --- /dev/null +++ b/desktop/src/ui/components/TimeAgo.tsx @@ -0,0 +1,48 @@ +import { type ParentProps, createSignal, onCleanup, onMount } from 'solid-js'; + +type TimeAgoProps = ParentProps & { + timestamp: number; +}; + +function TimeAgo(props: TimeAgoProps) { + const [time, setTime] = createSignal(''); + const [intervalId, setIntervalId] = createSignal(undefined); + + onMount(() => { + const formatter = new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' }); + const units = { + year: 24 * 60 * 60 * 1000 * 365, + month: 24 * 60 * 60 * 1000 * 365 / 12, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000, + }; + + const getRelativeTime = (timestamp: number): string => { + const elapsed = timestamp - Date.now(); + + for (const [unit, ms] of Object.entries(units)) { + if (Math.abs(elapsed) > ms || unit === 'second') + return formatter.format(Math.round(elapsed / ms), unit as Intl.RelativeTimeFormatUnit); + } + + return 'now'; + }; + + const loop = () => { + setTime(getRelativeTime(props.timestamp)); + }; + + loop(); + setIntervalId(setInterval(loop, 1000)); + }); + + onCleanup(() => { + clearInterval(intervalId()!); + }); + + return {time()}; +} + +export default TimeAgo; diff --git a/desktop/src/ui/components/native/WindowFrame.tsx b/desktop/src/ui/components/WindowFrame.tsx similarity index 87% rename from desktop/src/ui/components/native/WindowFrame.tsx rename to desktop/src/ui/components/WindowFrame.tsx index 7b307ac2..87ed3485 100644 --- a/desktop/src/ui/components/native/WindowFrame.tsx +++ b/desktop/src/ui/components/WindowFrame.tsx @@ -2,8 +2,8 @@ import { Window } from '@tauri-apps/api/window'; import { MinusIcon, XCloseIcon } from '@untitled-theme/icons-solid'; import type { JSX } from 'solid-js'; import { createSignal, onMount } from 'solid-js'; -import Modal from '../overlay/Modal'; -import Button from '../base/Button'; +import Modal from './overlay/Modal'; +import Button from './base/Button'; interface TitlebarButtonProps { icon: (any: any) => JSX.Element; @@ -50,8 +50,8 @@ function WindowFrame() { visible={isModalVisible} setVisible={setModalVisible} buttons={[ - , - , + , + , ]} />
diff --git a/desktop/src/ui/components/base/Button.module.scss b/desktop/src/ui/components/base/Button.module.scss index 60355886..21290884 100644 --- a/desktop/src/ui/components/base/Button.module.scss +++ b/desktop/src/ui/components/base/Button.module.scss @@ -21,7 +21,14 @@ } &__icon { - @apply p-1.5 hover:bg-gray-0.05 active:bg-gray-0.10 aspect-square box-border; + @apply p-1.5 hover:bg-gray-0.05 active:bg-gray-0.10 aspect-square box-border w-8 h-8; + + &.large { + @apply w-9 h-9; + & > * { + @apply p-0.5; + } + } & > * { @apply w-full h-full stroke-fg-primary; @@ -43,4 +50,4 @@ .icon > * { @apply w-4; -} +} \ No newline at end of file diff --git a/desktop/src/ui/components/base/Button.tsx b/desktop/src/ui/components/base/Button.tsx index e8506ebb..383449a3 100644 --- a/desktop/src/ui/components/base/Button.tsx +++ b/desktop/src/ui/components/base/Button.tsx @@ -5,15 +5,16 @@ import styles from './Button.module.scss'; type ButtonProps = JSX.ButtonHTMLAttributes & ParentProps & { iconLeft?: JSX.Element; iconRight?: JSX.Element; - styleType?: 'primary' | 'secondary' | 'danger' | 'icon' | 'ghost'; + buttonStyle?: 'primary' | 'secondary' | 'danger' | 'icon' | 'ghost'; + large?: boolean; }; function Button(props: ButtonProps) { - const [buttonProps, rest] = splitProps(props, ['iconLeft', 'iconRight', 'styleType', 'class']); - const merged = mergeProps({ styleType: 'primary' }, buttonProps); + const [buttonProps, rest] = splitProps(props, ['iconLeft', 'iconRight', 'buttonStyle', 'class', 'large']); + const merged = mergeProps({ buttonStyle: 'primary', large: false }, buttonProps); return ( -
{props.loggedIn && ( - )} @@ -50,11 +50,11 @@ function AccountPopup(props: Popup.PopupProps) {
- +
-
diff --git a/desktop/src/ui/components/overlay/Popup.tsx b/desktop/src/ui/components/overlay/Popup.tsx index c25cd7af..1a0dc98e 100644 --- a/desktop/src/ui/components/overlay/Popup.tsx +++ b/desktop/src/ui/components/overlay/Popup.tsx @@ -1,3 +1,4 @@ +import { mergeRefs } from '@solid-primitives/refs'; import { Show, createEffect, @@ -11,13 +12,13 @@ import { Portal } from 'solid-js/web'; // eslint-disable-next-line ts/no-namespace declare namespace Popup { - interface PopupProps extends ParentProps { + type PopupProps = Omit[0], 'children'> & ParentProps & { visible: Accessor; setVisible: Setter; class?: string; style?: JSX.CSSProperties | string; mount?: Node; - } + }; } function Popup(props: Popup.PopupProps) { @@ -53,8 +54,14 @@ function Popup(props: Popup.PopupProps) { return ( - -
+ { + el.classList.add('absolute', 'z-[1000]'); + return el; + }, props.ref)} + > +
{props.children}
@@ -62,4 +69,14 @@ function Popup(props: Popup.PopupProps) { ); } +Popup.setPos = (parent: HTMLElement, ref: (SVGGElement | HTMLDivElement)) => { + const parentRect = parent.getBoundingClientRect(); + + const top = parentRect.bottom; + const right = document.body.clientWidth - parentRect.right; + + ref.style.top = `${top}px`; + ref.style.right = `${right}px`; +}; + export default Popup; diff --git a/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx b/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx new file mode 100644 index 00000000..4400fdca --- /dev/null +++ b/desktop/src/ui/components/overlay/notifications/NotificationComponent.tsx @@ -0,0 +1,102 @@ +import { type JSX, Show } 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'; + +interface NotificationComponentProps { + overlay: boolean; + data: Core.Notification; +}; + +function IconFromNotificationType(type: NotificationType): (props: JSX.HTMLAttributes) => JSX.Element { + switch (type) { + case NotificationType.Alert: + return AlertTriangleIcon as any; + case NotificationType.Download: + return FolderDownloadIcon as any; + case NotificationType.DownloadSuccess: + return FolderCheckIcon as any; + case NotificationType.DownloadError: + return FolderXIcon as any; + case NotificationType.Refresh: + return RefreshCcw02Icon as any; + case NotificationType.Success: + return CheckCircleIcon as any; + case NotificationType.Info: + default: + return InfoCircleIcon as any; + } +} + +function ColorFromNotificationType(type: NotificationType): string { + switch (type) { + case NotificationType.Info: + case NotificationType.Refresh: + return 'text-brand'; + + case NotificationType.Alert: + case NotificationType.DownloadError: + return 'text-danger'; + + case NotificationType.DownloadSuccess: + case NotificationType.Success: + return 'text-success'; + + case NotificationType.Download: + default: + return 'text-fg-primary'; + } +} + +function NotificationOverlayComponent(props: NotificationComponentProps) { + return ( +
+ {props.data.title} +
+ ); +} + +function NotificationPopupComponent(props: NotificationComponentProps) { + return ( +
+
+ {IconFromNotificationType(props.data.notification_type)({ + class: `w-6 h-6 ${ColorFromNotificationType(props.data.notification_type)}`, + })} + +
+ {props.data.title} + {props.data.message} +
+ +
+ + + + +
+
+ + +
+
+
+ +
+ ); +} + +function NotificationComponent(props: NotificationComponentProps) { + return ( + <> + {(props.overlay ? NotificationOverlayComponent : NotificationPopupComponent)(props)} + + ); +} + +export default NotificationComponent; diff --git a/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx b/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx new file mode 100644 index 00000000..bb18399b --- /dev/null +++ b/desktop/src/ui/components/overlay/notifications/NotificationPopup.tsx @@ -0,0 +1,91 @@ +import { For, Match, Switch, createSignal, 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'; + +function NotificationPopup(props: Popup.PopupProps) { + const [notifications, setNotifications] = createSignal([]); + + let inner!: HTMLDivElement; + let parent!: HTMLDivElement; + + function updateSize() { + if (inner && parent) { + const rect = inner.getBoundingClientRect(); + parent.style.height = `${rect.height}px`; + } + } + + async function refetchNotifications() { + const notis = await manager.getNotifications(); + setNotifications(notis); + updateSize(); + console.log(notifications()); + } + + onMount(() => { + document.addEventListener('keypress', (e) => { + if (e.key === 'n') { + manager.addNotification({ + title: 'Test Notification', + message: 'This is a test notification', + notification_type: manager.NotificationType.Download, + progress: 0.39, + }); + } + }); + + refetchNotifications(); + manager.on('added', refetchNotifications); + manager.on('removed', refetchNotifications); + manager.on('cleared', refetchNotifications); + }); + + return ( + +
+
+
+

Notifications

+ + + + +
+ + {noti => ( +
+ + +
+ )} +
+
+
+
+ + You have no notifications + + +
+ +
+ + + +
+
+
+
+
+ ); +} + +export default NotificationPopup; diff --git a/desktop/src/ui/pages/Home.tsx b/desktop/src/ui/pages/Home.tsx index 19cb1162..55bc3334 100644 --- a/desktop/src/ui/pages/Home.tsx +++ b/desktop/src/ui/pages/Home.tsx @@ -47,7 +47,7 @@ function Banner() {
-
@@ -159,7 +159,7 @@ function ClusterCard(props: Core.ClusterWithManifest) { {props.instance.mods && `• ${props.mods} mods`} */}

-
@@ -218,7 +218,7 @@ function HomePage() {