Browse Source

Add client notification (#771)

* ClientNotification WIP

* Test

* ClientNotification WIP

* Add client notification dialog and related functionality

* Update ClientNotificationDialog.tsx

---------

Co-authored-by: philon- <[email protected]>
pull/775/head
Jeremy Gallant 10 months ago
committed by GitHub
parent
commit
32f31cb502
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      packages/core/src/utils/eventSystem.ts
  2. 12
      packages/core/src/utils/transform/decodePacket.ts
  3. 5
      packages/web/public/i18n/locales/en/dialog.json
  4. 72
      packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx
  5. 7
      packages/web/src/components/Dialog/DialogManager.tsx
  6. 6
      packages/web/src/core/stores/deviceStore.mock.ts
  7. 42
      packages/web/src/core/stores/deviceStore.ts
  8. 7
      packages/web/src/core/subscriptions.ts

9
packages/core/src/utils/eventSystem.ts

@ -330,6 +330,15 @@ export class EventSystem {
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
*

12
packages/core/src/utils/transform/decodePacket.ts

@ -209,6 +209,18 @@ export const decodePacket = (device: MeshDevice) =>
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: {
device.log.warn(
Types.Emitter[Types.Emitter.HandleFromRadio],

5
packages/web/public/i18n/locales/en/dialog.json

@ -184,5 +184,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."
}
}

72
packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx

@ -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>
);
};

7
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";
@ -84,6 +85,12 @@ export const DialogManager = () => {
setDialogOpen("deleteMessages", open);
}}
/>
<ClientNotificationDialog
open={dialog.clientNotification}
onOpenChange={(open) => {
setDialogOpen("clientNotification", open);
}}
/>
</>
);
};

6
packages/web/src/core/stores/deviceStore.mock.ts

@ -44,7 +44,10 @@ export const mockDeviceStore: Device = {
refreshKeys: false,
deleteMessages: false,
managedMode: false,
clientNotification: false,
},
clientNotifications: [],
setStatus: vi.fn(),
setConfig: vi.fn(),
setModuleConfig: vi.fn(),
@ -85,6 +88,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().mockReturnValue(0),
getUnreadCount: vi.fn().mockReturnValue(0),
};

42
packages/web/src/core/stores/deviceStore.ts

@ -62,7 +62,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;
@ -125,6 +127,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 {
@ -173,12 +182,14 @@ export const useDeviceStore = createStore<PrivateDeviceState>((set, get) => ({
refreshKeys: 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(
@ -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];
},
});
}),
);

7
packages/web/src/core/subscriptions.ts

@ -114,6 +114,13 @@ export const subscribeAll = (
});
});
connection.events.onClientNotificationPacket.subscribe(
(clientNotificationPacket) => {
device.addClientNotification(clientNotificationPacket);
device.setDialogOpen("clientNotification", true);
},
);
connection.events.onRoutingPacket.subscribe((routingPacket) => {
if (routingPacket.data.variant.case === "errorReason") {
switch (routingPacket.data.variant.value) {

Loading…
Cancel
Save