diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ddfa69..b082e4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- GUI: Remember acknowledged alerts and do not show them again. + ## [3.0.7] - 2025-10-04 ## [3.0.6] - 2025-10-02 diff --git a/src-gui/src/renderer/components/pages/swap/ApiAlertsBox.tsx b/src-gui/src/renderer/components/pages/swap/ApiAlertsBox.tsx index cee038f3..0e0a6a39 100644 --- a/src-gui/src/renderer/components/pages/swap/ApiAlertsBox.tsx +++ b/src-gui/src/renderer/components/pages/swap/ApiAlertsBox.tsx @@ -1,14 +1,14 @@ import { Box } from "@mui/material"; import { Alert, AlertTitle } from "@mui/material"; -import { removeAlert } from "store/features/alertsSlice"; -import { useAppDispatch, useAppSelector } from "store/hooks"; +import { acknowledgeAlert } from "store/features/alertsSlice"; +import { useAlerts, useAppDispatch } from "store/hooks"; export default function ApiAlertsBox() { - const alerts = useAppSelector((state) => state.alerts.alerts); + const alerts = useAlerts(); const dispatch = useAppDispatch(); - function onRemoveAlert(id: number) { - dispatch(removeAlert(id)); + function onAcknowledgeAlert(id: number) { + dispatch(acknowledgeAlert(id)); } if (alerts.length === 0) return null; @@ -20,7 +20,7 @@ export default function ApiAlertsBox() { variant="filled" severity={alert.severity} key={alert.id} - onClose={() => onRemoveAlert(alert.id)} + onClose={() => onAcknowledgeAlert(alert.id)} > {alert.title} {alert.body} diff --git a/src-gui/src/renderer/store/storeRenderer.ts b/src-gui/src/renderer/store/storeRenderer.ts index c19fec51..dce48138 100644 --- a/src-gui/src/renderer/store/storeRenderer.ts +++ b/src-gui/src/renderer/store/storeRenderer.ts @@ -17,7 +17,7 @@ import { LazyStore } from "@tauri-apps/plugin-store"; const rootPersistConfig = { key: "gui-global-state-store", storage: sessionStorage, - blacklist: ["settings", "conversations", "logs"], + blacklist: ["settings", "conversations", "alerts", "logs"], }; // Use Tauri's store plugin for persistent settings @@ -58,6 +58,12 @@ const conversationsPersistConfig = { storage: createTauriStorage(), }; +// Persist alerts across application restarts +const alertsPersistConfig = { + key: "alerts", + storage: createTauriStorage(), +}; + // Create a persisted version of the settings reducer const persistedSettingsReducer = persistReducer( settingsPersistConfig, @@ -70,11 +76,18 @@ const persistedConversationsReducer = persistReducer( reducers.conversations, ); +// Create a persisted version of the alerts reducer +const persistedAlertsReducer = persistReducer( + alertsPersistConfig, + reducers.alerts, +); + // Combine all reducers, using the persisted settings reducer const rootReducer = combineReducers({ ...reducers, settings: persistedSettingsReducer, conversations: persistedConversationsReducer, + alerts: persistedAlertsReducer, }); // Enable persistence for the entire application state diff --git a/src-gui/src/store/features/alertsSlice.ts b/src-gui/src/store/features/alertsSlice.ts index 170c4e61..2df35d82 100644 --- a/src-gui/src/store/features/alertsSlice.ts +++ b/src-gui/src/store/features/alertsSlice.ts @@ -1,14 +1,28 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Alert } from "models/apiModel"; +import { fnv1a } from "utils/hash"; export interface AlertsSlice { alerts: Alert[]; + /// The ids of the alerts that have been acknowledged + /// by the user and should not be shown again + acknowledgedAlerts: AcknowledgementKey[]; } const initialState: AlertsSlice = { alerts: [], + acknowledgedAlerts: [], }; +/// We use the key in combination with the fnv1a hash of the title +/// to uniquely identify an alert +/// +/// If the title changes, the hash will change and the alert will be shown again +interface AcknowledgementKey { + id: number; + titleHash: string; +} + const alertsSlice = createSlice({ name: "alerts", initialState, @@ -16,13 +30,21 @@ const alertsSlice = createSlice({ setAlerts(slice, action: PayloadAction) { slice.alerts = action.payload; }, - removeAlert(slice, action: PayloadAction) { - slice.alerts = slice.alerts.filter( - (alert) => alert.id !== action.payload, - ); + acknowledgeAlert(slice, action: PayloadAction) { + const alertTitle = slice.alerts.find( + (alert) => alert.id === action.payload, + )?.title; + + // If we cannot find the alert, we cannot acknowledge it + if (alertTitle != null) { + slice.acknowledgedAlerts.push({ + id: action.payload, + titleHash: fnv1a(alertTitle), + }); + } }, }, }); -export const { setAlerts, removeAlert } = alertsSlice.actions; +export const { setAlerts, acknowledgeAlert } = alertsSlice.actions; export default alertsSlice.reducer; diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index 18c02424..df6db140 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -30,6 +30,8 @@ import { TauriBackgroundProgress, TauriBitcoinSyncProgress, } from "models/tauriModel"; +import { Alert } from "models/apiModel"; +import { fnv1a } from "utils/hash"; export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; @@ -327,3 +329,17 @@ export function useTotalUnreadMessagesCount(): number { return totalUnreadCount; } + +/// Returns all the alerts that have not been acknowledged +export function useAlerts(): Alert[] { + return useAppSelector((state) => + state.alerts.alerts.filter( + (alert) => + // Check if there is an acknowledgement with + // the same id and the same title hash + !state.alerts.acknowledgedAlerts.some( + (ack) => ack.id === alert.id && ack.titleHash === fnv1a(alert.title), + ), + ), + ); +}