diff --git a/public/locales/de.json b/public/locales/de.json index 16b2d36..1bdd055 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Protokoll", + "Dialog.noLogs": "Keine Protokolle", "Notification.notificationTest": "Testbenachrichtigung", "Notification.succeedToTestNotification": "Die Testbenachrichtigung wurde erfolgreich gesendet.", diff --git a/public/locales/en.json b/public/locales/en.json index f87174f..52704a0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Log", + "Dialog.noLogs": "No Logs", "Notification.notificationTest": "Testing Notification", "Notification.succeedToTestNotification": "The testing notification was sent successfully.", diff --git a/public/locales/es-419.json b/public/locales/es-419.json index 07816e7..572096f 100644 --- a/public/locales/es-419.json +++ b/public/locales/es-419.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Registro", + "Dialog.noLogs": "Sin registros", "Notification.notificationTest": "Notificación de prueba", "Notification.succeedToTestNotification": "La notificación de prueba se envió correctamente.", diff --git a/public/locales/es.json b/public/locales/es.json index 607a2d4..557a455 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Registro", + "Dialog.noLogs": "Sin registros", "Notification.notificationTest": "Notificación de prueba", "Notification.succeedToTestNotification": "La notificación de prueba se envió correctamente.", diff --git a/public/locales/fr-CA.json b/public/locales/fr-CA.json index a06e338..18c43c6 100644 --- a/public/locales/fr-CA.json +++ b/public/locales/fr-CA.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Journal", + "Dialog.noLogs": "Aucun journal", "Notification.notificationTest": "Notification de test", "Notification.succeedToTestNotification": "La notification de test a été envoyée avec succès.", diff --git a/public/locales/fr.json b/public/locales/fr.json index 6f89971..6071780 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -50,6 +50,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Journal", + "Dialog.noLogs": "Aucun journal", "Notification.notificationTest": "Notification de test", "Notification.succeedToTestNotification": "La notification de test a été envoyée avec succès.", diff --git a/public/locales/it.json b/public/locales/it.json index d4c3845..4497085 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Registro", + "Dialog.noLogs": "Nessun registro", "Notification.notificationTest": "Notifica di prova", "Notification.succeedToTestNotification": "La notifica di prova è stata inviata con successo.", diff --git a/public/locales/ja.json b/public/locales/ja.json index 731412d..5faf1cf 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -49,6 +49,7 @@ "EggGraph.fail": "NG", "Dialog.log": "ログ", + "Dialog.noLogs": "ログがありません", "Notification.notificationTest": "通知テスト", "Notification.succeedToTestNotification": "通知テストに成功しました", diff --git a/public/locales/ko.json b/public/locales/ko.json index f804cc6..574bc94 100644 --- a/public/locales/ko.json +++ b/public/locales/ko.json @@ -49,6 +49,7 @@ "EggGraph.failure": "NG", "Dialog.log": "로그", + "Dialog.noLogs": "로그 없음", "Notification.notificationTest": "테스트 알림", "Notification.succeedToTestNotification": "테스트 알림이 성공적으로 전송되었습니다.", diff --git a/public/locales/nl.json b/public/locales/nl.json index 5eef0f3..75860cf 100644 --- a/public/locales/nl.json +++ b/public/locales/nl.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Logboek", + "Dialog.noLogs": "Geen logboeken", "Notification.notificationTest": "Testmelding", "Notification.succeedToTestNotification": "De testmelding is succesvol verzonden.", diff --git a/public/locales/ru.json b/public/locales/ru.json index 18d7daa..d5d52cd 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -49,6 +49,7 @@ "EggGraph.fail": "✘", "Dialog.log": "Журнал", + "Dialog.noLogs": "Нет журналов", "Notification.notificationTest": "Тестирование уведомления", "Notification.succeedToTestNotification": "Тестовое уведомление было успешно отправлено.", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index 87f3aac..60395f6 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -49,6 +49,7 @@ "EggGraph.fail": "NG", "Dialog.log": "日志", + "Dialog.noLogs": "没有日志", "Notification.notificationTest": "测试通知", "Notification.succeedToTestNotification": "测试通知已成功发送。", diff --git a/public/locales/zh-TW.json b/public/locales/zh-TW.json index 8f006e3..491d583 100644 --- a/public/locales/zh-TW.json +++ b/public/locales/zh-TW.json @@ -49,6 +49,7 @@ "EggGraph.fail": "NG", "Dialog.log": "日誌", + "Dialog.noLogs": "沒有日誌", "Notification.notificationTest": "測試通知", "Notification.succeedToTestNotification": "測試通知已成功發送。", diff --git a/src/app/store.ts b/src/app/store.ts index b9ce91d..adb5ada 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -2,6 +2,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit' import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from 'redux-persist' import persistStore from 'redux-persist/es/persistStore' +import autoCleanupLogs from '@/notification/middlewares/autoCleanupLogs' import log from '@/notification/slicers' import overlay from '@/overlay/slicers' import config from '@/settings/slicers' @@ -25,7 +26,7 @@ const store = configureStore({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], }, - }).concat(overlayMiddleware as any), + }).concat(overlayMiddleware as any, autoCleanupLogs), }) export const persistor = persistStore(store) diff --git a/src/modules/notification/components/NotificationController/index.tsx b/src/modules/notification/components/NotificationController/index.tsx index ae13944..d6b5da3 100644 --- a/src/modules/notification/components/NotificationController/index.tsx +++ b/src/modules/notification/components/NotificationController/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import { useIntl } from 'react-intl' import type { WebSocketLog } from '@/telemetry/hooks/websocket' @@ -11,10 +11,10 @@ import NotificationMessages from './messages' const SUPPRESS_DISCONNECT_DURATION = 2 * 60 * 1000 -const NotificationController = () => { +const NotificationController = function () { const intl = useIntl() const logs = useAppSelector(state => state.log.logs) - const [previousLogLength, setPreviousLogLength] = useState(0) + const [previousTimestamp, setPreviousTimestamp] = useState(Date.now()) const [previousDisconnectedTimestamp, setPreviousDisconnectedTimestamp] = useState(0) const savedRef = useRef(null) @@ -24,7 +24,14 @@ const NotificationController = () => { return } - for (let i = previousLogLength; i < logs.length; ++i) { + const targetIndex = logs.findIndex(function (log) { + return log.timestamp > previousTimestamp + }) + if (targetIndex === -1) { + return + } + + for (let i = targetIndex; i >= 0; --i) { const log = logs[i] let message: NotificationMessage @@ -63,10 +70,10 @@ const NotificationController = () => { } handler.publish(message) } - setPreviousLogLength(logs.length) + setPreviousTimestamp(logs[0].timestamp) }, [logs]) - return + return } -export default NotificationController +export default memo(NotificationController) diff --git a/src/modules/notification/components/NotificationHost/Notification.tsx b/src/modules/notification/components/NotificationHost/Notification.tsx index 13f1f65..01cc844 100644 --- a/src/modules/notification/components/NotificationHost/Notification.tsx +++ b/src/modules/notification/components/NotificationHost/Notification.tsx @@ -1,11 +1,13 @@ +import { memo } from 'react' + import { XMarkIcon } from '@heroicons/react/16/solid' import * as ToastPrimitive from '@radix-ui/react-toast' interface NotificationProps { - timestamp: number - title: string - description?: string - duration?: number + readonly timestamp: number + readonly title: string + readonly description?: string + readonly duration?: number onClose?(timestamp: number): void } @@ -47,4 +49,4 @@ const Notification = (props: NotificationProps) => { ) } -export default Notification +export default memo(Notification) diff --git a/src/modules/notification/components/NotificationHost/index.tsx b/src/modules/notification/components/NotificationHost/index.tsx index 66dc3b1..308c916 100644 --- a/src/modules/notification/components/NotificationHost/index.tsx +++ b/src/modules/notification/components/NotificationHost/index.tsx @@ -11,10 +11,10 @@ export interface NotificationHandle { } export interface NotificationMessage { - timestamp: number - title: string - description?: string - duration?: number + readonly timestamp: number + readonly title: string + readonly description?: string + readonly duration?: number } export interface NotificationHostProps { @@ -26,41 +26,50 @@ export const NotificationHost = forwardRef([]) - useImperativeHandle(forwardedRef, () => ({ - publish: (notification: NotificationMessage) => { - const length = notifications.length + 1 - if (maxCount && length > maxCount) { - const newNotifications = notifications.slice(length - maxCount) - newNotifications.push(notification) - setNotifications(newNotifications) - } else { - const newNotifications = [...notifications, notification] - setNotifications(newNotifications) - } - }, - })) + useImperativeHandle(forwardedRef, function () { + return { + publish(notification: NotificationMessage) { + const length = notifications.length + 1 + if (maxCount && length > maxCount) { + const newNotifications = notifications.slice(length - maxCount) + newNotifications.push(notification) + setNotifications(newNotifications) + } else { + const newNotifications = [...notifications, notification] + setNotifications(newNotifications) + } + }, + } satisfies NotificationHandle + }) - useEffect(() => { + useEffect(function () { if (isBottom && ref.current) { ref.current.scrollTop = ref.current.scrollHeight } }, [notifications]) - const handleScroll = (e: UIEvent) => { + const handleScroll = function (e: UIEvent) { const target = e.currentTarget const availableTop = target.scrollHeight - target.clientHeight setIsBottom(target.scrollTop === availableTop) } - const handleClose = useCallback((timestamp: number) => { + const handleClose = useCallback(function (timestamp: number) { + setNotifications(function (notifications) { const index = notifications.findIndex(n => n.timestamp === timestamp) - notifications.splice(index, 1) - }, []) + if (index !== -1) { + const newNotifications = notifications.slice(0) + newNotifications.splice(index, 1) + return newNotifications + } + return notifications + }) + }, [setNotifications]) return ( <> - {notifications.map(notification => { + {notifications.map(function (notification) { return ( .Notification-close { + opacity: 1; +} +.Notification-close:hover { + background-color: var(--ctl-bg); +} +.Notification-close:focus-visible { + outline: 2px solid #FFFC; +} +.Notification-close[disabled] { + color: var(--fg-secondary); + background-color: var(--ctl-bg--disable); +} - .Notification:hover > & { - opacity: 1; +@keyframes notificationSlideIn { + from { + transform: translateX(100%); } - - &:hover { - background-color: var(--ctl-bg); + to { + transform: translateX(0); } - &:focus-visible { - outline: 2px solid #FFFC; +} +@keyframes notificationSlideRight { + from { + transform: translateX(var(--radix-toast-swipe-end-x)); } - &[disabled] { - color: var(--fg-secondary); - background-color: var(--ctl-bg--disable); + to { + transform: translateX(100%); } } diff --git a/src/modules/notification/middlewares/autoCleanupLogs.ts b/src/modules/notification/middlewares/autoCleanupLogs.ts new file mode 100644 index 0000000..8ff1be5 --- /dev/null +++ b/src/modules/notification/middlewares/autoCleanupLogs.ts @@ -0,0 +1,22 @@ +import { Dispatch, MiddlewareAPI, UnknownAction } from 'redux' + +import { RootState } from 'app/store' + +import { addLog, cleanupLogs } from '../slicers' + +const autoCleanupLogs = function (store: MiddlewareAPI) { + return function (next: Dispatch) { + return function (action: UnknownAction) { + const result = next(action) + + const inAddLog = addLog.match(action) + if (inAddLog) { + store.dispatch(cleanupLogs()) + } + + return result + } + } +} + +export default autoCleanupLogs diff --git a/src/modules/notification/slicers/index.ts b/src/modules/notification/slicers/index.ts index d5c0568..e21d3fb 100644 --- a/src/modules/notification/slicers/index.ts +++ b/src/modules/notification/slicers/index.ts @@ -1,5 +1,9 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit' +// Constant +const MAX_LOG_ENTRIES = 63 +const LOG_EXPIRATION_HOURS = 3 + // State export type LogType = | 'test' @@ -7,8 +11,8 @@ export type LogType = | 'websocket_disconnect' export interface Log { - type: LogType - timestamp: number + readonly type: LogType + readonly timestamp: number } export interface LogState { @@ -25,12 +29,29 @@ const logSlice = createSlice({ initialState, reducers: { addLog(state, action: PayloadAction) { - state.logs.push(action.payload) + state.logs.unshift(action.payload) + }, + cleanupLogs(state) { + const current = Date.now() + const expirationTime = LOG_EXPIRATION_HOURS * 60 * 60 * 1000 + + const newLogs: Log[] = [] + for (let i = 0; i < state.logs.length; ++i) { + const log = state.logs[i] + if ((current - log.timestamp) <= expirationTime) { + newLogs.push(log) + if (newLogs.length >= MAX_LOG_ENTRIES) { + break + } + } + } + state.logs = newLogs }, }, }) export const { addLog, + cleanupLogs, } = logSlice.actions export default logSlice.reducer diff --git a/src/modules/settings/messages.ts b/src/modules/settings/messages.ts index dcf2fdc..7e4c8ef 100644 --- a/src/modules/settings/messages.ts +++ b/src/modules/settings/messages.ts @@ -127,6 +127,10 @@ const DialogMessages = defineMessages({ id: 'Dialog.log', defaultMessage: 'Log', }, + logNologs: { + id: 'Dialog.noLogs', + defaultMessage: 'No Logs', + }, general: { id: 'Dialog.general', diff --git a/src/modules/settings/pages/LogPage.tsx b/src/modules/settings/pages/LogPage.tsx index d345ada..d1a3350 100644 --- a/src/modules/settings/pages/LogPage.tsx +++ b/src/modules/settings/pages/LogPage.tsx @@ -22,15 +22,23 @@ const LogPage = () => { {intl.formatMessage(DialogMessages.log)} -
    - {logs.map((log, index) => { - return ( -
  • - {`${intl.formatDate(log.timestamp, opts)}: ${log.type}`} -
  • - ) - })} -
+ {logs.length === 0 + ? ( +
+

{intl.formatMessage(DialogMessages.logNologs)}

+
+ ) + : ( +
    + {logs.map((log, index) => { + return ( +
  • + {`${intl.formatDate(log.timestamp, opts)}: ${log.type}`} +
  • + ) + })} +
+ )} ) }