feat(gui): Remember acknowledged alerts and do not show them again

This commit is contained in:
Binarybaron 2025-10-05 22:51:20 +02:00
parent bbdae0c18c
commit 65a46a4205
5 changed files with 65 additions and 12 deletions

View file

@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- GUI: Remember acknowledged alerts and do not show them again.
## [3.0.7] - 2025-10-04 ## [3.0.7] - 2025-10-04
## [3.0.6] - 2025-10-02 ## [3.0.6] - 2025-10-02

View file

@ -1,14 +1,14 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { Alert, AlertTitle } from "@mui/material"; import { Alert, AlertTitle } from "@mui/material";
import { removeAlert } from "store/features/alertsSlice"; import { acknowledgeAlert } from "store/features/alertsSlice";
import { useAppDispatch, useAppSelector } from "store/hooks"; import { useAlerts, useAppDispatch } from "store/hooks";
export default function ApiAlertsBox() { export default function ApiAlertsBox() {
const alerts = useAppSelector((state) => state.alerts.alerts); const alerts = useAlerts();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
function onRemoveAlert(id: number) { function onAcknowledgeAlert(id: number) {
dispatch(removeAlert(id)); dispatch(acknowledgeAlert(id));
} }
if (alerts.length === 0) return null; if (alerts.length === 0) return null;
@ -20,7 +20,7 @@ export default function ApiAlertsBox() {
variant="filled" variant="filled"
severity={alert.severity} severity={alert.severity}
key={alert.id} key={alert.id}
onClose={() => onRemoveAlert(alert.id)} onClose={() => onAcknowledgeAlert(alert.id)}
> >
<AlertTitle>{alert.title}</AlertTitle> <AlertTitle>{alert.title}</AlertTitle>
{alert.body} {alert.body}

View file

@ -17,7 +17,7 @@ import { LazyStore } from "@tauri-apps/plugin-store";
const rootPersistConfig = { const rootPersistConfig = {
key: "gui-global-state-store", key: "gui-global-state-store",
storage: sessionStorage, storage: sessionStorage,
blacklist: ["settings", "conversations", "logs"], blacklist: ["settings", "conversations", "alerts", "logs"],
}; };
// Use Tauri's store plugin for persistent settings // Use Tauri's store plugin for persistent settings
@ -58,6 +58,12 @@ const conversationsPersistConfig = {
storage: createTauriStorage(), storage: createTauriStorage(),
}; };
// Persist alerts across application restarts
const alertsPersistConfig = {
key: "alerts",
storage: createTauriStorage(),
};
// Create a persisted version of the settings reducer // Create a persisted version of the settings reducer
const persistedSettingsReducer = persistReducer( const persistedSettingsReducer = persistReducer(
settingsPersistConfig, settingsPersistConfig,
@ -70,11 +76,18 @@ const persistedConversationsReducer = persistReducer(
reducers.conversations, reducers.conversations,
); );
// Create a persisted version of the alerts reducer
const persistedAlertsReducer = persistReducer(
alertsPersistConfig,
reducers.alerts,
);
// Combine all reducers, using the persisted settings reducer // Combine all reducers, using the persisted settings reducer
const rootReducer = combineReducers({ const rootReducer = combineReducers({
...reducers, ...reducers,
settings: persistedSettingsReducer, settings: persistedSettingsReducer,
conversations: persistedConversationsReducer, conversations: persistedConversationsReducer,
alerts: persistedAlertsReducer,
}); });
// Enable persistence for the entire application state // Enable persistence for the entire application state

View file

@ -1,14 +1,28 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Alert } from "models/apiModel"; import { Alert } from "models/apiModel";
import { fnv1a } from "utils/hash";
export interface AlertsSlice { export interface AlertsSlice {
alerts: Alert[]; 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 = { const initialState: AlertsSlice = {
alerts: [], 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({ const alertsSlice = createSlice({
name: "alerts", name: "alerts",
initialState, initialState,
@ -16,13 +30,21 @@ const alertsSlice = createSlice({
setAlerts(slice, action: PayloadAction<Alert[]>) { setAlerts(slice, action: PayloadAction<Alert[]>) {
slice.alerts = action.payload; slice.alerts = action.payload;
}, },
removeAlert(slice, action: PayloadAction<number>) { acknowledgeAlert(slice, action: PayloadAction<number>) {
slice.alerts = slice.alerts.filter( const alertTitle = slice.alerts.find(
(alert) => alert.id !== action.payload, (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; export default alertsSlice.reducer;

View file

@ -30,6 +30,8 @@ import {
TauriBackgroundProgress, TauriBackgroundProgress,
TauriBitcoinSyncProgress, TauriBitcoinSyncProgress,
} from "models/tauriModel"; } from "models/tauriModel";
import { Alert } from "models/apiModel";
import { fnv1a } from "utils/hash";
export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@ -327,3 +329,17 @@ export function useTotalUnreadMessagesCount(): number {
return totalUnreadCount; 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),
),
),
);
}