diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 9c28d989a1..76ad557516 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -13,7 +13,6 @@ import { getSettingsPrefixAtom, getTabIndicatorAtom, globalStore, - isDev, } from "@/store/global"; import { appHandleKeyDown, keyboardMouseDownHandler } from "@/store/keymodel"; import { getElemAsStr } from "@/util/focusutil"; @@ -29,7 +28,6 @@ import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { AppBackground } from "./app-bg"; import { CenteredDiv } from "./element/quickelems"; -import { NotificationBubbles } from "./notification/notificationbubbles"; import "./app.scss"; @@ -267,7 +265,6 @@ const AppInner = () => { - {isDev() ? : null} ); }; diff --git a/frontend/app/notification/notificationbubbles.scss b/frontend/app/notification/notificationbubbles.scss deleted file mode 100644 index 345fa40e8e..0000000000 --- a/frontend/app/notification/notificationbubbles.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -.notification-bubbles { - display: flex; - width: 380px; - flex-direction: column; - align-items: flex-start; - row-gap: 8px; -} diff --git a/frontend/app/notification/notificationbubbles.tsx b/frontend/app/notification/notificationbubbles.tsx deleted file mode 100644 index d5f612efc5..0000000000 --- a/frontend/app/notification/notificationbubbles.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { atoms } from "@/store/global"; -import { FloatingPortal, useFloating, useInteractions } from "@floating-ui/react"; -import clsx from "clsx"; -import { useAtomValue } from "jotai"; -import { useEffect, useState } from "react"; -import "./notificationbubbles.scss"; -import { NotificationItem } from "./notificationitem"; -import { useNotification } from "./usenotification"; - -const NotificationBubbles = () => { - const { - notifications, - hoveredId, - hideNotification, - copyNotification, - handleActionClick, - formatTimestamp, - setHoveredId, - } = useNotification(); - const [isOpen, setIsOpen] = useState(notifications.length > 0); - const notificationPopoverMode = useAtomValue(atoms.notificationPopoverMode); - - useEffect(() => { - setIsOpen(notifications.length > 0); - }, [notifications.length]); - - const { refs, strategy } = useFloating({ - open: isOpen, - onOpenChange: setIsOpen, - strategy: "fixed", - }); - - const { getFloatingProps } = useInteractions(); - - const floatingStyles = { - position: strategy, - right: "58px", - bottom: "10px", - top: "auto", - left: "auto", - }; - - if (!isOpen || notificationPopoverMode) { - return null; - } - - return ( - -
e.stopPropagation(), - })} - > - {notifications.map((notif) => { - if (notif.hidden) return null; - return ( - setHoveredId(notif.id)} - onMouseLeave={() => setHoveredId(null)} - isBubble={true} - /> - ); - })} -
-
- ); -}; - -export { NotificationBubbles }; diff --git a/frontend/app/notification/notificationitem.scss b/frontend/app/notification/notificationitem.scss deleted file mode 100644 index 2dcf2a3a21..0000000000 --- a/frontend/app/notification/notificationitem.scss +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -.notification { - &.hovered { - background: #292929; - } - - .notification-title { - font-size: 13px; - font-style: normal; - font-weight: 500; - line-height: 18px; - margin-bottom: 3px; - - &.green { - color: var(--success-color); - } - - &.red { - color: var(--error-color); - } - - &.yellow { - color: var(--warning-color); - } - } - - .notification-message { - font-size: 13px; - font-style: normal; - font-weight: 400; - line-height: 18px; - opacity: 0.7; - } - - .notification-actions { - display: flex; - gap: 10px; - flex-wrap: wrap; - margin-top: 10px; - - i { - margin-left: 3px; - font-size: 11px; - } - } - - .close-btn { - position: absolute; - top: 5px; - right: 5px; - } - - .lock-btn { - position: absolute; - top: 5px; - right: 5px; - padding: 10px 8px; - font-size: 11px; - color: rgb(from var(--main-text-color) r g b / 0.5); - } - - .notification { - width: 100%; - color: var(--main-text-color); - padding: 12px 10px; - display: flex; - flex-direction: column; - position: relative; - } - - .notification-bubble { - position: relative; - display: flex; - width: 380px; - padding: 16px 24px 16px 16px; - align-items: flex-start; - gap: 12px; - border-radius: 8px; - border: 0.5px solid rgba(255, 255, 255, 0.12); - background: #232323; - box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25); - } - - .notification-inner { - display: flex; - align-items: flex-start; - column-gap: 6px; - - .notification-icon { - margin-right: 5px; - margin-top: 1px; - - i { - font-size: 16px; - } - - i.green { - color: var(--success-color); - } - - i.red { - color: var(--error-color); - } - - i.yellow { - color: var(--warning-color); - } - } - - .notification-timestamp { - font-size: 12px; - font-weight: 400; - line-height: 18px; - opacity: 0.5; - margin-bottom: 7px; - } - } -} diff --git a/frontend/app/notification/notificationitem.tsx b/frontend/app/notification/notificationitem.tsx deleted file mode 100644 index 738b3f0c99..0000000000 --- a/frontend/app/notification/notificationitem.tsx +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "@/element/button"; -import { makeIconClass } from "@/util/util"; -import clsx from "clsx"; - -import "./notificationitem.scss"; -interface NotificationItemProps { - notification: NotificationType; - onRemove: (id: string) => void; - onCopy: (id: string) => void; - onActionClick: (e: React.MouseEvent, action: NotificationActionType, id: string) => void; - formatTimestamp: (timestamp: string) => string; - isBubble: boolean; - className?: string; - onMouseEnter?: () => void; - onMouseLeave?: () => void; -} - -const NotificationItem = ({ - notification, - onRemove, - onCopy, - onActionClick, - formatTimestamp, - isBubble, - className, - onMouseEnter, - onMouseLeave, -}: NotificationItemProps) => { - const { id, title, message, icon, type, timestamp, persistent, actions } = notification; - const color = type === "error" ? "red" : type === "warning" ? "yellow" : "green"; - const nIcon = icon ? icon : "bell"; - - const renderCloseButton = () => { - if (!isBubble && persistent) { - return ( - - - - ); - } - return ( - - ); - }; - - return ( -
onCopy(id)} - title="Click to Copy Notification Message" - > - {renderCloseButton()} -
- {nIcon && ( -
- -
- )} -
- {title &&
{title}
} - {timestamp && !isBubble && ( -
{formatTimestamp(timestamp)}
- )} - {message &&
{message}
} - {actions && actions.length > 0 && ( -
- {actions.map((action, index) => ( - - ))} -
- )} -
-
-
- ); -}; - -export { NotificationItem }; diff --git a/frontend/app/notification/notificationpopover.tsx b/frontend/app/notification/notificationpopover.tsx deleted file mode 100644 index b62c8c4986..0000000000 --- a/frontend/app/notification/notificationpopover.tsx +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { Button } from "@/element/button"; -import { Popover, PopoverButton, PopoverContent } from "@/element/popover"; -import { atoms } from "@/store/global"; -import { makeIconClass } from "@/util/util"; -import clsx from "clsx"; -import { useAtom } from "jotai"; -import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; -import { Fragment, useCallback } from "react"; -import { NotificationItem } from "./notificationitem"; -import { useUpdateNotifier } from "./updatenotifier"; -import { useNotification } from "./usenotification"; - -const NotificationPopover = () => { - useUpdateNotifier(); - const { - notifications, - removeNotification, - removeAllNotifications, - hideAllNotifications, - copyNotification, - handleActionClick, - formatTimestamp, - hoveredId, - setHoveredId, - } = useNotification(); - const [notificationPopoverMode, setNotificationPopoverMode] = useAtom(atoms.notificationPopoverMode); - - const handleTogglePopover = useCallback(() => { - if (notificationPopoverMode) { - hideAllNotifications(); - } - setNotificationPopoverMode(!notificationPopoverMode); - }, [notificationPopoverMode]); - - const hasErrors = notifications.some((n) => n.type === "error"); - const hasUpdate = notifications.some((n) => n.type === "update"); - - const addOnClassNames = hasUpdate ? "solid green" : hasErrors ? "solid red" : "ghost grey"; - - const getIcon = () => { - if (hasUpdate) { - return ; - } - return ; - }; - - return ( - - i]:text-[17px] px-[6px] py-[4px]", - addOnClassNames - )} - disabled={notifications.length === 0} - onClick={handleTogglePopover} - > - {getIcon()} - - {notifications.length > 0 && ( - -
- Notifications - -
- - {notifications.map((notif, index) => ( - - setHoveredId(notif.id)} - onMouseLeave={() => setHoveredId(null)} - /> - {index !== notifications.length - 1 &&
} -
- ))} -
-
- )} -
- ); -}; - -export { NotificationPopover }; diff --git a/frontend/app/notification/updatenotifier.tsx b/frontend/app/notification/updatenotifier.tsx deleted file mode 100644 index 40392bb0e8..0000000000 --- a/frontend/app/notification/updatenotifier.tsx +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { atoms, isDev, pushNotification } from "@/store/global"; -import { useAtomValue } from "jotai"; -import { useEffect } from "react"; - -export const useUpdateNotifier = () => { - const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom); - - useEffect(() => { - let notification: NotificationType | null = null; - - switch (appUpdateStatus) { - case "ready": - notification = { - id: "update-notification", - icon: "arrows-rotate", - title: "Update Available", - message: "A new update is available and ready to be installed.", - timestamp: new Date().toLocaleString(), - type: "update", - actions: [ - { - label: "Install Now", - actionKey: "installUpdate", - color: "green", - disabled: false, - }, - ], - }; - break; - - case "downloading": - notification = { - id: "update-notification", - icon: "arrows-rotate", - title: "Downloading Update", - message: "The update is currently being downloaded.", - timestamp: new Date().toLocaleString(), - type: "update", - actions: [ - { - label: "Downloading...", - actionKey: "", - color: "green", - disabled: true, - }, - ], - }; - break; - - case "installing": - notification = { - id: "update-notification", - icon: "arrows-rotate", - title: "Installing Update", - message: "The update is currently being installed.", - timestamp: new Date().toLocaleString(), - type: "update", - actions: [ - { - label: "Installing...", - actionKey: "", - color: "green", - disabled: true, - }, - ], - }; - break; - - case "error": - notification = { - id: "update-notification", - icon: "circle-exclamation", - title: "Update Error", - message: "An error occurred during the update process.", - timestamp: new Date().toLocaleString(), - type: "update", - actions: [ - { - label: "Retry Update", - actionKey: "retryUpdate", - color: "green", - disabled: false, - }, - ], - }; - break; - } - - if (!isDev()) return; - - if (notification) { - pushNotification(notification); - } - }, [appUpdateStatus]); -}; diff --git a/frontend/app/notification/usenotification.tsx b/frontend/app/notification/usenotification.tsx deleted file mode 100644 index b98d2fd1b5..0000000000 --- a/frontend/app/notification/usenotification.tsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { atoms, getApi } from "@/store/global"; -import { useAtom, useAtomValue } from "jotai"; -import { useCallback, useEffect, useState } from "react"; - -const notificationActions: { [key: string]: () => void } = { - installUpdate: () => { - getApi().installAppUpdate(); - }, - // Add other action functions here -}; - -export function useNotification() { - const notificationPopoverMode = useAtomValue(atoms.notificationPopoverMode); - const [notifications, setNotifications] = useAtom(atoms.notifications); - const [hoveredId, setHoveredId] = useState(null); - - const removeNotification = useCallback( - (id: string) => { - setNotifications((prevNotifications) => prevNotifications.filter((n) => n.id !== id)); - }, - [setNotifications] - ); - - const hideNotification = useCallback( - (id: string) => { - setNotifications((prevNotifications) => - prevNotifications.map((n) => (n.id === id ? { ...n, hidden: true } : n)) - ); - }, - [setNotifications] - ); - - const hideAllNotifications = useCallback(() => { - setNotifications((prevNotifications) => prevNotifications.map((n) => ({ ...n, hidden: true }))); - }, [setNotifications]); - - const removeAllNotifications = useCallback(() => { - setNotifications((prevNotifications) => prevNotifications.filter((n) => n.persistent)); - }, [setNotifications]); - - const copyNotification = useCallback( - (id: string) => { - const notif = notifications.find((n) => n.id === id); - if (!notif) return; - - let text = notif.title ?? ""; - if (notif.message) { - text += text.length > 0 ? `\n${notif.message}` : notif.message; - } - navigator.clipboard - .writeText(text) - .then(() => { - console.info("Text copied to clipboard"); - }) - .catch((err) => { - console.error("Failed to copy text: ", err); - }); - }, - [notifications] - ); - - const handleActionClick = useCallback( - (e: React.MouseEvent, action: NotificationActionType, id: string) => { - e.stopPropagation(); - const actionFn = notificationActions[action.actionKey]; - if (actionFn) { - actionFn(); - removeNotification(id); - } else { - console.warn(`No action found for key: ${action.actionKey}`); - } - }, - [removeNotification] - ); - - useEffect(() => { - if (notificationPopoverMode) { - return; - } - - const hasExpiringNotifications = notifications.some((notif) => notif.expiration); - if (!hasExpiringNotifications) { - return; - } - - const intervalId = setInterval(() => { - const now = Date.now(); - - setNotifications((prevNotifications) => - prevNotifications.filter( - (notif) => !notif.expiration || notif.expiration > now || notif.id === hoveredId - ) - ); - }, 1000); - - return () => clearInterval(intervalId); - }, [notificationPopoverMode, notifications, hoveredId, setNotifications]); - - const formatTimestamp = (timestamp: string): string => { - const notificationTime = new Date(timestamp).getTime(); - const now = Date.now(); - const diffInSeconds = Math.floor((now - notificationTime) / 1000); - const diffInMinutes = Math.floor(diffInSeconds / 60); - const diffInHours = Math.floor(diffInMinutes / 60); - const diffInDays = Math.floor(diffInHours / 24); - - if (diffInMinutes === 0) { - return `Just now`; - } else if (diffInMinutes < 60) { - return `${diffInMinutes} mins ago`; - } else if (diffInHours < 24) { - return `${diffInHours} hrs ago`; - } else if (diffInDays < 7) { - return `${diffInDays} days ago`; - } else { - return new Date(timestamp).toLocaleString(); - } - }; - - return { - notifications, - hoveredId, - setHoveredId, - removeNotification, - removeAllNotifications, - hideNotification, - hideAllNotifications, - copyNotification, - handleActionClick, - formatTimestamp, - }; -} diff --git a/frontend/app/store/global-atoms.ts b/frontend/app/store/global-atoms.ts index 1e1c4a84d0..d0e6950877 100644 --- a/frontend/app/store/global-atoms.ts +++ b/frontend/app/store/global-atoms.ts @@ -119,8 +119,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const connStatuses = Array.from(connStatusMap.values()).map((atom) => get(atom)); return connStatuses; }); - const notificationsAtom = atom([]); - const notificationPopoverModeAtom = atom(false); const reinitVersion = atom(0); const rateLimitInfoAtom = atom(null) as PrimitiveAtom; atoms = { @@ -143,8 +141,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { documentHasFocus: documentHasFocusAtom, modalOpen, allConnStatus: allConnStatusAtom, - notifications: notificationsAtom, - notificationPopoverMode: notificationPopoverModeAtom, reinitVersion, waveAIRateLimitInfoAtom: rateLimitInfoAtom, } as GlobalAtomsType; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index e46626448e..2cfb5945ae 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -736,35 +736,6 @@ function clearAllTabIndicators() { } } -function addOrUpdateNotification(notif: NotificationType) { - globalStore.set(atoms.notifications, (prevNotifications) => { - // Remove any existing notification with the same ID - const notificationsWithoutThisId = prevNotifications.filter((n) => n.id !== notif.id); - // Add the new notification - return [...notificationsWithoutThisId, notif]; - }); -} - -function pushNotification(notif: NotificationType) { - if (!notif.id && notif.persistent) { - return; - } - notif.id = notif.id ?? crypto.randomUUID(); - addOrUpdateNotification(notif); -} - -function removeNotificationById(id: string) { - globalStore.set(atoms.notifications, (prev) => { - return prev.filter((notif) => notif.id !== id); - }); -} - -function removeNotification(id: string) { - globalStore.set(atoms.notifications, (prev) => { - return prev.filter((notif) => notif.id !== id); - }); -} - function createTab() { getApi().createTab(); } @@ -815,13 +786,10 @@ export { loadConnStatus, loadTabIndicators, openLink, - pushNotification, readAtom, recordTEvent, refocusNode, registerBlockComponentModel, - removeNotification, - removeNotificationById, replaceBlock, setActiveTab, setNodeFocus, diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index d37d6b4ce0..12e2827119 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -25,8 +25,6 @@ declare global { updaterStatusAtom: jotai.PrimitiveAtom; modalOpen: jotai.PrimitiveAtom; allConnStatus: jotai.Atom; - notifications: jotai.PrimitiveAtom; - notificationPopoverMode: jotai.Atom; reinitVersion: jotai.PrimitiveAtom; waveAIRateLimitInfoAtom: jotai.PrimitiveAtom; }; @@ -407,27 +405,6 @@ declare global { baseDir: string; }; - export type NotificationActionType = { - label: string; - actionKey: string; - rightIcon?: string; - color?: "green" | "grey"; - disabled?: boolean; - }; - - export type NotificationType = { - id?: string; - icon: string; - title: string; - message: string; - timestamp: string; - expiration?: number; - hidden?: boolean; - actions?: NotificationActionType[]; - persistent?: boolean; - type?: "error" | "update" | "info" | "warning"; - }; - interface AbstractWshClient { recvRpcMessage(msg: RpcMessage): void; } diff --git a/frontend/wave.ts b/frontend/wave.ts index 341b36f48e..c380163071 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -26,8 +26,6 @@ import { initGlobalWaveEventSubs, loadConnStatus, loadTabIndicators, - pushNotification, - removeNotificationById, subscribeToConnEvents, } from "@/store/global"; import { activeTabIdAtom } from "@/store/tab-model"; @@ -49,8 +47,6 @@ let savedInitOpts: WaveInitOpts = null; (window as any).countersPrint = countersPrint; (window as any).countersClear = countersClear; (window as any).getLayoutModelForStaticTab = getLayoutModelForStaticTab; -(window as any).pushNotification = pushNotification; -(window as any).removeNotificationById = removeNotificationById; (window as any).modalsModel = modalsModel; function updateZoomFactor(zoomFactor: number) {