diff --git a/packages/web/public/i18n/locales/en/dialog.json b/packages/web/public/i18n/locales/en/dialog.json index b443e7e3..a5fa2250 100644 --- a/packages/web/public/i18n/locales/en/dialog.json +++ b/packages/web/public/i18n/locales/en/dialog.json @@ -179,5 +179,10 @@ "confirmUnderstanding": "Yes, I know what I'm doing", "title": "Are you sure?", "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + }, + "clientNotification": { + "title": "Client Notification", + "TraceRoute can only be sent once every 30 seconds": "TraceRoute can only be sent once every 30 seconds", + "Compromised keys were detected and regenerated.": "Compromised keys were detected and regenerated." } } diff --git a/packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx b/packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx new file mode 100644 index 00000000..1df5ef4f --- /dev/null +++ b/packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx @@ -0,0 +1,69 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog.tsx"; +import { useDevice } from "@core/stores/deviceStore.ts"; +import { useTranslation } from "react-i18next"; + +export interface ClientNotificationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export const ClientNotificationDialog = ({ + open, + onOpenChange, +}: ClientNotificationDialogProps) => { + const { t } = useTranslation("dialog"); + const { getClientNotification, removeClientNotification } = useDevice(); + + const localOnOpenChange = (open: boolean) => { + if (!getClientNotification(1)) { + onOpenChange(open); + } + removeClientNotification(0); + }; + + const dialogContent = (() => { + if (!getClientNotification(0)) { + return; + } + + switch (getClientNotification(0)?.payloadVariant.case) { + // TODO: Add KeyVerification logic + /*case "keyVerificationNumberInform": + return <>; + case "keyVerificationNumberRequest": + return <>; + case "keyVerificationFinal": + return <>; + case "duplicatedPublicKey": + return <>; + case "lowEntropyKey": + return <>;*/ + + default: + return ( + + {t("clientNotification.title")} + + {t(`clientNotification.${getClientNotification(0)?.message}`)} + + + ); + } + })(); + + return ( + + + + {dialogContent} + + + ); +}; diff --git a/packages/web/src/components/Dialog/DialogManager.tsx b/packages/web/src/components/Dialog/DialogManager.tsx index 0ba1a97f..f155523f 100644 --- a/packages/web/src/components/Dialog/DialogManager.tsx +++ b/packages/web/src/components/Dialog/DialogManager.tsx @@ -1,3 +1,4 @@ +import { ClientNotificationDialog } from "@components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx"; import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx"; import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx"; import { ImportDialog } from "@components/Dialog/ImportDialog.tsx"; @@ -91,6 +92,12 @@ export const DialogManager = () => { setDialogOpen("deleteMessages", open); }} /> + { + setDialogOpen("clientNotification", open); + }} + /> ); }; diff --git a/packages/web/src/core/stores/deviceStore.mock.ts b/packages/web/src/core/stores/deviceStore.mock.ts index d1dc70f8..22523511 100644 --- a/packages/web/src/core/stores/deviceStore.mock.ts +++ b/packages/web/src/core/stores/deviceStore.mock.ts @@ -45,7 +45,10 @@ export const mockDeviceStore: Device = { refreshKeys: false, deleteMessages: false, managedMode: false, + clientNotification: false, }, + clientNotifications: [], + setStatus: vi.fn(), setConfig: vi.fn(), setModuleConfig: vi.fn(), @@ -86,4 +89,9 @@ export const mockDeviceStore: Device = { sendAdminMessage: vi.fn(), updateFavorite: vi.fn(), updateIgnored: vi.fn(), + addClientNotification: vi.fn(), + removeClientNotification: vi.fn(), + getClientNotification: vi.fn(), + getAllUnreadCount: vi.fn(), + getUnreadCount: vi.fn(), }; diff --git a/packages/web/src/core/stores/deviceStore.ts b/packages/web/src/core/stores/deviceStore.ts index 4cbad192..8a561b16 100644 --- a/packages/web/src/core/stores/deviceStore.ts +++ b/packages/web/src/core/stores/deviceStore.ts @@ -63,7 +63,9 @@ export interface Device { refreshKeys: boolean; deleteMessages: boolean; managedMode: boolean; + clientNotification: boolean; }; + clientNotifications: Protobuf.Mesh.ClientNotification[]; setStatus: (status: Types.DeviceStatusEnum) => void; setConfig: (config: Protobuf.Config.Config) => void; @@ -126,6 +128,13 @@ export interface Device { sendAdminMessage: (message: Protobuf.Admin.AdminMessage) => void; updateFavorite: (nodeNum: number, isFavorite: boolean) => void; updateIgnored: (nodeNum: number, isIgnored: boolean) => void; + addClientNotification: ( + clientNotificationPacket: Protobuf.Mesh.ClientNotification, + ) => void; + removeClientNotification: (index: number) => void; + getClientNotification: ( + index: number, + ) => Protobuf.Mesh.ClientNotification | undefined; } export interface DeviceState { @@ -175,12 +184,14 @@ export const useDeviceStore = createStore((set, get) => ({ rebootOTA: false, deleteMessages: false, managedMode: false, + clientNotification: false, }, pendingSettingsChanges: false, messageDraft: "", nodeErrors: new Map(), unreadCounts: new Map(), nodesMap: new Map(), + clientNotifications: [], setStatus: (status: Types.DeviceStatusEnum) => { set( @@ -849,6 +860,37 @@ export const useDeviceStore = createStore((set, get) => ({ }), ); }, + addClientNotification: ( + clientNotificationPacket: Protobuf.Mesh.ClientNotification, + ) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) { + return; + } + device.clientNotifications.push(clientNotificationPacket); + }), + ); + }, + removeClientNotification: (index: number) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (!device) { + return; + } + device.clientNotifications.splice(index, 1); + }), + ); + }, + getClientNotification: (index: number) => { + const device = get().devices.get(id); + if (!device) { + return; + } + return device.clientNotifications[index]; + }, }); }), ); diff --git a/packages/web/src/core/subscriptions.ts b/packages/web/src/core/subscriptions.ts index e9725688..0f685f4e 100644 --- a/packages/web/src/core/subscriptions.ts +++ b/packages/web/src/core/subscriptions.ts @@ -116,7 +116,8 @@ export const subscribeAll = ( connection.events.onClientNotificationPacket.subscribe( (clientNotificationPacket) => { - console.debug(clientNotificationPacket.message); + device.addClientNotification(clientNotificationPacket); + device.setDialogOpen("clientNotification", true); }, );