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),
+ ),
+ ),
+ );
+}