Add client notification (#771)
* ClientNotification WIP * Test * ClientNotification WIP * Add client notification dialog and related functionality * Update ClientNotificationDialog.tsx --------- Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
co-authored by
philon-
parent
176d554ef9
commit
32f31cb502
@@ -330,6 +330,15 @@ export class EventSystem {
|
|||||||
PacketMetadata<Uint8Array>
|
PacketMetadata<Uint8Array>
|
||||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when a new MeshPacket message containing a ClientNotification packet has been
|
||||||
|
* received from device
|
||||||
|
*
|
||||||
|
* @event onClientNotificationPacket
|
||||||
|
*/
|
||||||
|
public readonly onClientNotificationPacket: SimpleEventDispatcher<Protobuf.Mesh.ClientNotification> =
|
||||||
|
new SimpleEventDispatcher<Protobuf.Mesh.ClientNotification>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires when the devices connection or configuration status changes
|
* Fires when the devices connection or configuration status changes
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -209,6 +209,18 @@ export const decodePacket = (device: MeshDevice) =>
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "clientNotification": {
|
||||||
|
device.log.trace(
|
||||||
|
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||||
|
`📣 Received ClientNotification: ${decodedMessage.payloadVariant.value.message}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
device.events.onClientNotificationPacket.dispatch(
|
||||||
|
decodedMessage.payloadVariant.value,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
device.log.warn(
|
device.log.warn(
|
||||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||||
|
|||||||
@@ -184,5 +184,10 @@
|
|||||||
"confirmUnderstanding": "Yes, I know what I'm doing",
|
"confirmUnderstanding": "Yes, I know what I'm doing",
|
||||||
"title": "Are you sure?",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
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) => {
|
||||||
|
removeClientNotification(0);
|
||||||
|
if (!getClientNotification(0)) {
|
||||||
|
onOpenChange(open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("clientNotification.title")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t([
|
||||||
|
`clientNotification.${getClientNotification(0)?.message}`,
|
||||||
|
getClientNotification(0)?.message ?? "",
|
||||||
|
])}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={localOnOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogClose />
|
||||||
|
{dialogContent}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ClientNotificationDialog } from "@components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx";
|
||||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||||
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
||||||
@@ -84,6 +85,12 @@ export const DialogManager = () => {
|
|||||||
setDialogOpen("deleteMessages", open);
|
setDialogOpen("deleteMessages", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ClientNotificationDialog
|
||||||
|
open={dialog.clientNotification}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen("clientNotification", open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ export const mockDeviceStore: Device = {
|
|||||||
refreshKeys: false,
|
refreshKeys: false,
|
||||||
deleteMessages: false,
|
deleteMessages: false,
|
||||||
managedMode: false,
|
managedMode: false,
|
||||||
|
clientNotification: false,
|
||||||
},
|
},
|
||||||
|
clientNotifications: [],
|
||||||
|
|
||||||
setStatus: vi.fn(),
|
setStatus: vi.fn(),
|
||||||
setConfig: vi.fn(),
|
setConfig: vi.fn(),
|
||||||
setModuleConfig: vi.fn(),
|
setModuleConfig: vi.fn(),
|
||||||
@@ -85,6 +88,9 @@ export const mockDeviceStore: Device = {
|
|||||||
sendAdminMessage: vi.fn(),
|
sendAdminMessage: vi.fn(),
|
||||||
updateFavorite: vi.fn(),
|
updateFavorite: vi.fn(),
|
||||||
updateIgnored: vi.fn(),
|
updateIgnored: vi.fn(),
|
||||||
|
addClientNotification: vi.fn(),
|
||||||
|
removeClientNotification: vi.fn(),
|
||||||
|
getClientNotification: vi.fn(),
|
||||||
getAllUnreadCount: vi.fn().mockReturnValue(0),
|
getAllUnreadCount: vi.fn().mockReturnValue(0),
|
||||||
getUnreadCount: vi.fn().mockReturnValue(0),
|
getUnreadCount: vi.fn().mockReturnValue(0),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ export interface Device {
|
|||||||
refreshKeys: boolean;
|
refreshKeys: boolean;
|
||||||
deleteMessages: boolean;
|
deleteMessages: boolean;
|
||||||
managedMode: boolean;
|
managedMode: boolean;
|
||||||
|
clientNotification: boolean;
|
||||||
};
|
};
|
||||||
|
clientNotifications: Protobuf.Mesh.ClientNotification[];
|
||||||
|
|
||||||
setStatus: (status: Types.DeviceStatusEnum) => void;
|
setStatus: (status: Types.DeviceStatusEnum) => void;
|
||||||
setConfig: (config: Protobuf.Config.Config) => void;
|
setConfig: (config: Protobuf.Config.Config) => void;
|
||||||
@@ -125,6 +127,13 @@ export interface Device {
|
|||||||
sendAdminMessage: (message: Protobuf.Admin.AdminMessage) => void;
|
sendAdminMessage: (message: Protobuf.Admin.AdminMessage) => void;
|
||||||
updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
|
updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
|
||||||
updateIgnored: (nodeNum: number, isIgnored: 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 {
|
export interface DeviceState {
|
||||||
@@ -173,12 +182,14 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
|
|||||||
refreshKeys: false,
|
refreshKeys: false,
|
||||||
deleteMessages: false,
|
deleteMessages: false,
|
||||||
managedMode: false,
|
managedMode: false,
|
||||||
|
clientNotification: false,
|
||||||
},
|
},
|
||||||
pendingSettingsChanges: false,
|
pendingSettingsChanges: false,
|
||||||
messageDraft: "",
|
messageDraft: "",
|
||||||
nodeErrors: new Map(),
|
nodeErrors: new Map(),
|
||||||
unreadCounts: new Map(),
|
unreadCounts: new Map(),
|
||||||
nodesMap: new Map(),
|
nodesMap: new Map(),
|
||||||
|
clientNotifications: [],
|
||||||
|
|
||||||
setStatus: (status: Types.DeviceStatusEnum) => {
|
setStatus: (status: Types.DeviceStatusEnum) => {
|
||||||
set(
|
set(
|
||||||
@@ -847,6 +858,37 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
addClientNotification: (
|
||||||
|
clientNotificationPacket: Protobuf.Mesh.ClientNotification,
|
||||||
|
) => {
|
||||||
|
set(
|
||||||
|
produce<PrivateDeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device.clientNotifications.push(clientNotificationPacket);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeClientNotification: (index: number) => {
|
||||||
|
set(
|
||||||
|
produce<PrivateDeviceState>((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];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -114,6 +114,13 @@ export const subscribeAll = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connection.events.onClientNotificationPacket.subscribe(
|
||||||
|
(clientNotificationPacket) => {
|
||||||
|
device.addClientNotification(clientNotificationPacket);
|
||||||
|
device.setDialogOpen("clientNotification", true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||||
if (routingPacket.data.variant.case === "errorReason") {
|
if (routingPacket.data.variant.case === "errorReason") {
|
||||||
switch (routingPacket.data.variant.value) {
|
switch (routingPacket.data.variant.value) {
|
||||||
|
|||||||
Reference in New Issue
Block a user