feat: add error handling for key mismatch
This commit is contained in:
@@ -8,6 +8,7 @@ import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
|||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog.tsx";
|
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog.tsx";
|
||||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||||
|
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
||||||
|
|
||||||
export const DialogManager = () => {
|
export const DialogManager = () => {
|
||||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||||
@@ -70,6 +71,12 @@ export const DialogManager = () => {
|
|||||||
setDialogOpen("unsafeRoles", open);
|
setDialogOpen("unsafeRoles", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<RefreshKeysDialog
|
||||||
|
open={dialog.refreshKeys}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen("refreshKeys", open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||||
|
import { RefreshKeysDialog } from "./RefreshKeysDialog";
|
||||||
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
|
||||||
|
vi.mock("./useRefreshKeysDialog.ts", () => ({
|
||||||
|
useRefreshKeysDialog: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("RefreshKeysDialog Component", () => {
|
||||||
|
let handleCloseDialogMock: Mock;
|
||||||
|
let handleNodeRemoveMock: Mock;
|
||||||
|
let onOpenChangeMock: Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handleCloseDialogMock = vi.fn();
|
||||||
|
handleNodeRemoveMock = vi.fn();
|
||||||
|
onOpenChangeMock = vi.fn();
|
||||||
|
|
||||||
|
(useRefreshKeysDialog as Mock).mockReturnValue({
|
||||||
|
handleCloseDialog: handleCloseDialogMock,
|
||||||
|
handleNodeRemove: handleNodeRemoveMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the dialog with correct content", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
expect(screen.getByText("Keys Mismatch")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Request New Keys")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Dismiss")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls handleNodeRemove when 'Request New Keys' button is clicked", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
fireEvent.click(screen.getByText("Request New Keys"));
|
||||||
|
expect(handleNodeRemoveMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls handleCloseDialog when 'Dismiss' button is clicked", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
fireEvent.click(screen.getByText("Dismiss"));
|
||||||
|
expect(handleCloseDialogMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onOpenChange when dialog close button is clicked", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /close/i }));
|
||||||
|
expect(handleCloseDialogMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when open is false", () => {
|
||||||
|
render(<RefreshKeysDialog open={false} onOpenChange={onOpenChangeMock} />);
|
||||||
|
expect(screen.queryByText("Keys Mismatch")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog.tsx";
|
||||||
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
|
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||||
|
import { P } from "@components/UI/Typography/P.tsx";
|
||||||
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
|
||||||
|
export interface RefreshKeysDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
|
||||||
|
|
||||||
|
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-8 flex flex-col gap-2">
|
||||||
|
<DialogClose onClick={handleCloseDialog} />
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Keys Mismatch</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
Your node is unable to send a direct message to this node. This is due to public/private key mismatch.
|
||||||
|
<ul className="mt-2">
|
||||||
|
<li className="flex place-items-center gap-2 items-start">
|
||||||
|
<div className="p-2 bg-slate-500 rounded-lg mt-2">
|
||||||
|
<LockKeyholeOpenIcon size={30} className="text-white justify-center" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2" >
|
||||||
|
<P className="font-bold">Refresh this node</P>
|
||||||
|
<p>
|
||||||
|
This will remove the node from the chat and request new keys. The process may take a few moments to complete.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleNodeRemove}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
Request New Keys
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCloseDialog}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{/* </DialogDescription> */}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog >
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
|
vi.mock("@core/stores/appStore.ts", () => ({
|
||||||
|
useAppStore: vi.fn(() => ({ activeChat: "chat-123" })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@core/stores/deviceStore.ts", () => ({
|
||||||
|
useDevice: vi.fn(() => ({
|
||||||
|
removeNode: vi.fn(),
|
||||||
|
setDialogOpen: vi.fn(),
|
||||||
|
getNodeError: vi.fn(),
|
||||||
|
clearNodeError: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("useRefreshKeysDialog Hook", () => {
|
||||||
|
let removeNodeMock: Mock;
|
||||||
|
let setDialogOpenMock: Mock;
|
||||||
|
let getNodeErrorMock: Mock;
|
||||||
|
let clearNodeErrorMock: Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
removeNodeMock = vi.fn();
|
||||||
|
setDialogOpenMock = vi.fn();
|
||||||
|
getNodeErrorMock = vi.fn();
|
||||||
|
clearNodeErrorMock = vi.fn();
|
||||||
|
|
||||||
|
(useDevice as Mock).mockReturnValue({
|
||||||
|
removeNode: removeNodeMock,
|
||||||
|
setDialogOpen: setDialogOpenMock,
|
||||||
|
getNodeError: getNodeErrorMock,
|
||||||
|
clearNodeError: clearNodeErrorMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handleNodeRemove should remove the node and update dialog if there is an error", () => {
|
||||||
|
getNodeErrorMock.mockReturnValue({ node: "node-abc" });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleNodeRemove();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||||
|
expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||||
|
expect(removeNodeMock).toHaveBeenCalledWith("node-abc");
|
||||||
|
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handleNodeRemove should do nothing if there is no error", () => {
|
||||||
|
getNodeErrorMock.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleNodeRemove();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(removeNodeMock).not.toHaveBeenCalled();
|
||||||
|
expect(setDialogOpenMock).not.toHaveBeenCalled();
|
||||||
|
expect(clearNodeErrorMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handleCloseDialog should close the dialog", () => {
|
||||||
|
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleCloseDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
|
export function useRefreshKeysDialog() {
|
||||||
|
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice();
|
||||||
|
const { activeChat } = useAppStore();
|
||||||
|
|
||||||
|
const handleNodeRemove = useCallback(() => {
|
||||||
|
const nodeWithError = getNodeError(activeChat);
|
||||||
|
if (!nodeWithError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearNodeError(activeChat);
|
||||||
|
handleCloseDialog();;
|
||||||
|
return removeNode(nodeWithError?.node);
|
||||||
|
}, [activeChat, clearNodeError, setDialogOpen, removeNode]);
|
||||||
|
|
||||||
|
const handleCloseDialog = useCallback(() => {
|
||||||
|
setDialogOpen('refreshKeys', false);
|
||||||
|
}, [setDialogOpen])
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCloseDialog,
|
||||||
|
handleNodeRemove
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,9 +18,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
|
|||||||
setSerialPorts(await navigator?.serial.getPorts());
|
setSerialPorts(await navigator?.serial.getPorts());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
navigator?.serial?.addEventListener("connect", (event) => {
|
navigator?.serial?.addEventListener("connect", () => {
|
||||||
console.log(event);
|
|
||||||
|
|
||||||
updateSerialPortList();
|
updateSerialPortList();
|
||||||
});
|
});
|
||||||
navigator?.serial?.addEventListener("disconnect", () => {
|
navigator?.serial?.addEventListener("disconnect", () => {
|
||||||
@@ -47,8 +45,6 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
|
|||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<div className="flex w-full flex-col gap-2 p-4">
|
||||||
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
||||||
{serialPorts.map((port, index) => {
|
{serialPorts.map((port, index) => {
|
||||||
console.log(port);
|
|
||||||
|
|
||||||
const { usbProductId, usbVendorId } = port.getInfo();
|
const { usbProductId, usbVendorId } = port.getInfo();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ const EmptyState = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ChannelChat = ({
|
export const ChannelChat = ({
|
||||||
messages,
|
messages = [],
|
||||||
}: ChannelChatProps) => {
|
}: ChannelChatProps) => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export const ChannelChat = ({
|
|||||||
className="flex-1 overflow-y-auto pl-4 pr-4 md:pr-44"
|
className="flex-1 overflow-y-auto pl-4 pr-4 md:pr-44"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col justify-end min-h-full">
|
<div className="flex flex-col justify-end min-h-full">
|
||||||
{messages.map((message, index) => (
|
{messages?.map((message, index) => (
|
||||||
<Message
|
<Message
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipArrow,
|
TooltipArrow,
|
||||||
@@ -12,15 +13,13 @@ import {
|
|||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
import { AlertCircle, CheckCircle2, CircleEllipsis, LucideIcon } from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
const MESSAGE_STATES = {
|
type MessageStateValue = {
|
||||||
ACK: "ack",
|
state: string;
|
||||||
WAITING: "waiting",
|
icon: LucideIcon;
|
||||||
FAILED: "failed",
|
displayText: string;
|
||||||
} as const;
|
}
|
||||||
|
|
||||||
type MessageState = MessageWithState["state"];
|
type MessageState = MessageWithState["state"];
|
||||||
|
|
||||||
@@ -40,31 +39,36 @@ interface StatusIconProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_TEXT_MAP: Record<MessageState, string> = {
|
const MESSAGE_STATES: Record<string, MessageStateValue> = {
|
||||||
[MESSAGE_STATES.ACK]: "Message delivered",
|
ACK: { state: 'ack', icon: CheckCircle2, displayText: "Message delivered" },
|
||||||
[MESSAGE_STATES.WAITING]: "Waiting for delivery",
|
WAITING: { state: 'waiting', icon: CircleEllipsis, displayText: "Waiting for delivery" },
|
||||||
[MESSAGE_STATES.FAILED]: "Delivery failed",
|
FAILED: { state: 'failed', icon: AlertCircle, displayText: "Delivery failed" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_ICON_MAP: Record<MessageState, LucideIcon> = {
|
const getMessageState = (state: MessageState): MessageStateValue => {
|
||||||
[MESSAGE_STATES.ACK]: CheckCircle2,
|
switch (state) {
|
||||||
[MESSAGE_STATES.WAITING]: CircleEllipsis,
|
case MESSAGE_STATES.ACK.state:
|
||||||
[MESSAGE_STATES.FAILED]: AlertCircle,
|
return MESSAGE_STATES.ACK;
|
||||||
};
|
case MESSAGE_STATES.WAITING.state:
|
||||||
|
return MESSAGE_STATES.WAITING;
|
||||||
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
|
case MESSAGE_STATES.FAILED.state:
|
||||||
|
return MESSAGE_STATES.FAILED;
|
||||||
|
default:
|
||||||
|
return MESSAGE_STATES.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white dark:text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
side="top"
|
side="top"
|
||||||
align="center"
|
align="center"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
>
|
>
|
||||||
{getStatusText(state)}
|
{getMessageState(state).displayText ?? "An unknown error occurred"};
|
||||||
<TooltipArrow className="fill-slate-800" />
|
<TooltipArrow className="fill-slate-800" />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -72,13 +76,17 @@ const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
const msgState = getMessageState(state);
|
||||||
|
|
||||||
|
const isFailed = msgState.state === 'failed'
|
||||||
|
|
||||||
const iconClass = cn(
|
const iconClass = cn(
|
||||||
className,
|
className,
|
||||||
"text-slate-500 dark:text-slate-400 w-4 h-4 shrink-0",
|
"text-slate-500 dark:text-slate-400 size-5 shrink-0"
|
||||||
);
|
);
|
||||||
|
|
||||||
const Icon = STATUS_ICON_MAP[state];
|
const Icon = msgState.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusTooltip state={state}>
|
<StatusTooltip state={state}>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -90,23 +98,7 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMessageTextStyles = (state: MessageState) => {
|
const TimeDisplay = memo(({ date, className }: { date: Date; className?: string }) => (
|
||||||
const isAcknowledged = state === MESSAGE_STATES.ACK;
|
|
||||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
|
||||||
|
|
||||||
return cn(
|
|
||||||
"break-words overflow-hidden",
|
|
||||||
isAcknowledged
|
|
||||||
? "text-slate-900 dark:text-white"
|
|
||||||
: "text-slate-900 dark:text-slate-400",
|
|
||||||
isFailed && "text-red-500 dark:text-red-500",
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimeDisplay = ({
|
|
||||||
date,
|
|
||||||
className,
|
|
||||||
}: { date: Date; className?: string }) => (
|
|
||||||
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||||
{date.toLocaleDateString()}
|
{date.toLocaleDateString()}
|
||||||
@@ -118,9 +110,9 @@ const TimeDisplay = ({
|
|||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
));
|
||||||
|
|
||||||
export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
|
|
||||||
const isDeviceUser = useMemo(
|
const isDeviceUser = useMemo(
|
||||||
@@ -128,33 +120,47 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
|||||||
getDevices()
|
getDevices()
|
||||||
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
||||||
.includes(message.from),
|
.includes(message.from),
|
||||||
[getDevices, message.from],
|
[getDevices, message.from]
|
||||||
);
|
);
|
||||||
|
|
||||||
const messageUser = sender?.user;
|
const messageUser = sender?.user;
|
||||||
|
|
||||||
const messageTextClass = getMessageTextStyles(message.state);
|
const getMessageTextStyles = (state: MessageState) => {
|
||||||
|
const msgState = getMessageState(state);
|
||||||
|
const isAcknowledged = msgState.state === 'ack'
|
||||||
|
const isFailed = msgState.state === 'failed'
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
"break-words overflow-hidden",
|
||||||
|
isAcknowledged
|
||||||
|
? "text-slate-900 dark:text-white"
|
||||||
|
: "text-slate-900 dark:text-slate-400",
|
||||||
|
isFailed && "text-red-500 dark:text-red-500",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageTextClass = useMemo(() => getMessageTextStyles(message.state), [message.state]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full px-4 justify-start">
|
<div className="flex flex-col w-full px-4 justify-start">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col flex-wrap items-start py-1",
|
"flex flex-col flex-wrap items-start py-1",
|
||||||
isDeviceUser && "items-end",
|
isDeviceUser && "items-end"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{!lastMsgSameUser
|
{!lastMsgSameUser && (
|
||||||
? (
|
<div className="flex place-items-center gap-2 mb-1">
|
||||||
<div className="flex place-items-center gap-2 mb-1">
|
<Avatar text={messageUser?.shortName ?? "UNK"} />
|
||||||
<Avatar text={messageUser?.shortName ?? "UNK"} />
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col">
|
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
{messageUser?.longName}
|
||||||
{messageUser?.longName}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
: null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<TimeDisplay date={message.rxTime} />
|
<TimeDisplay date={message.rxTime} />
|
||||||
<div className="flex place-items-center gap-2 pb-2">
|
<div className="flex place-items-center gap-2 pb-2">
|
||||||
@@ -166,4 +172,4 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipArrow,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@components/UI/Tooltip.tsx";
|
||||||
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||||
|
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { ReactNode, useMemo } from "react";
|
||||||
|
import { Message, MessageState } from "@core/services/types.ts";
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
lastMsgSameUser: boolean;
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageStatus {
|
||||||
|
state: MessageState;
|
||||||
|
displayText: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MESSAGE_STATUS: Record<MessageState, MessageStatus> = {
|
||||||
|
ack: { state: "ack", displayText: "Message delivered", icon: CheckCircle2 },
|
||||||
|
waiting: { state: "waiting", displayText: "Waiting for delivery", icon: CircleEllipsis },
|
||||||
|
failed: { state: "failed", displayText: "Delivery failed", icon: AlertCircle },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageStatus = (state: MessageState): MessageStatus =>
|
||||||
|
MESSAGE_STATUS[state] || { state: "failed", displayText: "Unknown error", icon: AlertCircle };
|
||||||
|
|
||||||
|
const StatusTooltip = ({ status, children }: { status: MessageStatus; children: ReactNode }) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
{status.displayText}
|
||||||
|
<TooltipArrow className="fill-slate-800" />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StatusIcon = ({ status, className, ...otherProps }: { status: MessageStatus; className?: string }) => {
|
||||||
|
const isFailed = status.state === "failed";
|
||||||
|
const iconClass = cn("text-slate-500 dark:text-slate-400 w-4 h-4 shrink-0", className);
|
||||||
|
const Icon = status.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusTooltip status={status}>
|
||||||
|
<Icon className={iconClass} {...otherProps} color={isFailed ? "red" : "currentColor"} />
|
||||||
|
</StatusTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageTextStyles = (status: MessageStatus) => {
|
||||||
|
const isAcknowledged = status.state === "ack";
|
||||||
|
const isFailed = status.state === "failed";
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
"break-words overflow-hidden",
|
||||||
|
isAcknowledged ? "text-slate-900 dark:text-white" : "text-slate-900 dark:text-slate-400",
|
||||||
|
isFailed && "text-red-500 dark:text-red-500",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeDisplay = ({ date, className }: { date: Date; className?: string }) => (
|
||||||
|
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">{date.toLocaleDateString()}</span>
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||||
|
{date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => {
|
||||||
|
const { getDevices } = useDeviceStore();
|
||||||
|
|
||||||
|
const isDeviceUser = useMemo(
|
||||||
|
() =>
|
||||||
|
getDevices()
|
||||||
|
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
||||||
|
.includes(message.from),
|
||||||
|
[getDevices, message.from],
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageUser = message?.from
|
||||||
|
? getDevices().find((device) => device.nodes.has(message.from))?.nodes.get(message.from)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const messageStatus = getMessageStatus(message.state);
|
||||||
|
const messageTextClass = getMessageTextStyles(messageStatus);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full px-4 justify-start">
|
||||||
|
<div className={cn("flex flex-col flex-wrap items-start py-1", messageTextClass, isDeviceUser && "items-end")}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
{!lastMsgSameUser && (
|
||||||
|
<div className="flex place-items-center gap-2 mb-1">
|
||||||
|
<Avatar text={messageUser?.user?.shortName ?? "UNK"} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||||
|
{messageUser?.user?.longName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<TimeDisplay date={message.date} />
|
||||||
|
<div className="flex place-items-center gap-2 pb-2">
|
||||||
|
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>{message.message}</div>
|
||||||
|
<StatusIcon status={messageStatus} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cn } from "../../core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
import { LockKeyholeOpenIcon } from 'lucide-react';
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
type RGBColor = {
|
type RGBColor = {
|
||||||
@@ -12,6 +13,7 @@ interface AvatarProps {
|
|||||||
text: string;
|
text: string;
|
||||||
size?: "sm" | "lg";
|
size?: "sm" | "lg";
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/complexity/noStaticOnlyClass: stop being annoying Biome
|
// biome-ignore lint/complexity/noStaticOnlyClass: stop being annoying Biome
|
||||||
@@ -43,6 +45,7 @@ class ColorUtils {
|
|||||||
export const Avatar: React.FC<AvatarProps> = ({
|
export const Avatar: React.FC<AvatarProps> = ({
|
||||||
text,
|
text,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
|
showError = false,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const sizes = {
|
const sizes = {
|
||||||
@@ -73,12 +76,11 @@ export const Avatar: React.FC<AvatarProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`
|
`flex
|
||||||
|
relative
|
||||||
rounded-full
|
rounded-full
|
||||||
flex
|
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
size-11
|
|
||||||
font-semibold`,
|
font-semibold`,
|
||||||
sizes[size],
|
sizes[size],
|
||||||
className,
|
className,
|
||||||
@@ -88,6 +90,7 @@ export const Avatar: React.FC<AvatarProps> = ({
|
|||||||
color: textColor,
|
color: textColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{showError ? <LockKeyholeOpenIcon className="size-4 absolute bottom-0 right-0 z-10 text-red-500 stroke-3" /> : null}
|
||||||
<p className="p-1">{initials}</p>
|
<p className="p-1">{initials}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const buttonVariants = cva(
|
|||||||
success:
|
success:
|
||||||
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
||||||
outline:
|
outline:
|
||||||
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-400 dark:text-slate-500",
|
"bg-transparent border border-slate-400 hover:bg-slate-100 dark:border-slate-400 dark:text-slate-500",
|
||||||
subtle:
|
subtle:
|
||||||
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
||||||
ghost:
|
ghost:
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ export type DialogVariant =
|
|||||||
| "nodeRemoval"
|
| "nodeRemoval"
|
||||||
| "pkiBackup"
|
| "pkiBackup"
|
||||||
| "nodeDetails"
|
| "nodeDetails"
|
||||||
| "unsafeRoles";
|
| "unsafeRoles"
|
||||||
|
| "refreshKeys";
|
||||||
|
|
||||||
type QueueStatus = {
|
type QueueStatus = {
|
||||||
res: number, free: number, maxlen: number
|
res: number, free: number, maxlen: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeError = {
|
||||||
|
node: number;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: number;
|
id: number;
|
||||||
status: Types.DeviceStatusEnum;
|
status: Types.DeviceStatusEnum;
|
||||||
@@ -52,6 +58,7 @@ export interface Device {
|
|||||||
number,
|
number,
|
||||||
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
|
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
|
||||||
>;
|
>;
|
||||||
|
nodeErrors: Map<number, NodeError>;
|
||||||
connection?: MeshDevice;
|
connection?: MeshDevice;
|
||||||
activePage: Page;
|
activePage: Page;
|
||||||
activeNode: number;
|
activeNode: number;
|
||||||
@@ -71,6 +78,7 @@ export interface Device {
|
|||||||
pkiBackup: boolean;
|
pkiBackup: boolean;
|
||||||
nodeDetails: boolean;
|
nodeDetails: boolean;
|
||||||
unsafeRoles: boolean;
|
unsafeRoles: boolean;
|
||||||
|
refreshKeys: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +117,10 @@ export interface Device {
|
|||||||
processPacket: (data: ProcessPacketParams) => void;
|
processPacket: (data: ProcessPacketParams) => void;
|
||||||
setMessageDraft: (message: string) => void;
|
setMessageDraft: (message: string) => void;
|
||||||
setQueueStatus: (status: QueueStatus) => void;
|
setQueueStatus: (status: QueueStatus) => void;
|
||||||
|
setNodeError: (nodeNum: number, error: string) => void;
|
||||||
|
clearNodeError: (nodeNum: number) => void;
|
||||||
|
getNodeError: (nodeNum: number) => NodeError | undefined;
|
||||||
|
hasNodeError: (nodeNum: number) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceState {
|
export interface DeviceState {
|
||||||
@@ -162,9 +174,12 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
pkiBackup: false,
|
pkiBackup: false,
|
||||||
nodeDetails: false,
|
nodeDetails: false,
|
||||||
unsafeRoles: false,
|
unsafeRoles: false,
|
||||||
|
refreshKeys: false,
|
||||||
},
|
},
|
||||||
pendingSettingsChanges: false,
|
pendingSettingsChanges: false,
|
||||||
messageDraft: "",
|
messageDraft: "",
|
||||||
|
nodeErrors: new Map(),
|
||||||
|
|
||||||
|
|
||||||
setStatus: (status: Types.DeviceStatusEnum) => {
|
setStatus: (status: Types.DeviceStatusEnum) => {
|
||||||
set(
|
set(
|
||||||
@@ -531,7 +546,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
addTraceRoute: (traceroute) => {
|
addTraceRoute: (traceroute) => {
|
||||||
set(
|
set(
|
||||||
produce<DeviceState>((draft) => {
|
produce<DeviceState>((draft) => {
|
||||||
console.log("addTraceRoute called");
|
|
||||||
const device = draft.devices.get(id);
|
const device = draft.devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return;
|
return;
|
||||||
@@ -568,10 +582,8 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
) => {
|
) => {
|
||||||
set(
|
set(
|
||||||
produce<DeviceState>((draft) => {
|
produce<DeviceState>((draft) => {
|
||||||
console.log("setMessageState called");
|
|
||||||
const device = draft.devices.get(id);
|
const device = draft.devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
console.log("no device found for id");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const messageGroup = device.messages[type];
|
const messageGroup = device.messages[type];
|
||||||
@@ -582,7 +594,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
const messages = messageGroup.get(messageIndex);
|
const messages = messageGroup.get(messageIndex);
|
||||||
|
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
console.log("no messages found for id");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +674,42 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
setNodeError: (nodeNum, error) => {
|
||||||
|
set(
|
||||||
|
produce<DeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (device) {
|
||||||
|
device.nodeErrors.set(nodeNum, { node: nodeNum, error });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
clearNodeError: (nodeNum: number) => {
|
||||||
|
set(
|
||||||
|
produce<DeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (device) {
|
||||||
|
device.nodeErrors.delete(nodeNum);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getNodeError: (nodeNum: number) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Device not found");
|
||||||
|
}
|
||||||
|
return device.nodeErrors.get(nodeNum);
|
||||||
|
},
|
||||||
|
hasNodeError: (nodeNum: number) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Device not found");
|
||||||
|
}
|
||||||
|
return device.nodeErrors.has(nodeNum);
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
+19
-10
@@ -22,15 +22,15 @@ export const subscribeAll = (
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`Routing Error: ${routingPacket.data.variant.value}`);
|
console.info(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "routeReply": {
|
case "routeReply": {
|
||||||
console.log(`Route Reply: ${routingPacket.data.variant.value}`);
|
console.info(`Route Reply: ${routingPacket.data.variant.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "routeRequest": {
|
case "routeRequest": {
|
||||||
console.log(`Route Request: ${routingPacket.data.variant.value}`);
|
console.info(`Route Request: ${routingPacket.data.variant.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,6 @@ export const subscribeAll = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
connection.events.onChannelPacket.subscribe((channel) => {
|
connection.events.onChannelPacket.subscribe((channel) => {
|
||||||
console.log('channel', channel);
|
|
||||||
|
|
||||||
device.addChannel(channel);
|
device.addChannel(channel);
|
||||||
});
|
});
|
||||||
connection.events.onConfigPacket.subscribe((config) => {
|
connection.events.onConfigPacket.subscribe((config) => {
|
||||||
@@ -81,10 +79,8 @@ export const subscribeAll = (
|
|||||||
device.setModuleConfig(moduleConfig);
|
device.setModuleConfig(moduleConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
||||||
|
|
||||||
console.log('messagePacket', messagePacket);
|
|
||||||
|
|
||||||
device.addMessage({
|
device.addMessage({
|
||||||
...messagePacket,
|
...messagePacket,
|
||||||
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
|
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
|
||||||
@@ -111,8 +107,21 @@ export const subscribeAll = (
|
|||||||
|
|
||||||
connection.events.onQueueStatus.subscribe((queueStatus) => {
|
connection.events.onQueueStatus.subscribe((queueStatus) => {
|
||||||
device.setQueueStatus(queueStatus);
|
device.setQueueStatus(queueStatus);
|
||||||
if (queueStatus.free < 10) {
|
});
|
||||||
// start queueing messages
|
|
||||||
|
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||||
|
if (routingPacket.data.variant.case === "errorReason") {
|
||||||
|
switch (routingPacket.data.variant.value) {
|
||||||
|
case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
|
||||||
|
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||||
|
device.setNodeError(routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value]);
|
||||||
|
device.setDialogOpen("refreshKeys", true);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import { getChannelName } from "@pages/Channels.tsx";
|
|||||||
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
||||||
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
export const MessagesPage = () => {
|
export const MessagesPage = () => {
|
||||||
const { channels, nodes, hardware, messages } = useDevice();
|
const { channels, nodes, hardware, messages, hasNodeError } = useDevice();
|
||||||
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||||
@@ -82,6 +83,8 @@ export const MessagesPage = () => {
|
|||||||
element={
|
element={
|
||||||
<Avatar
|
<Avatar
|
||||||
text={node.user?.shortName ?? node.num.toString()}
|
text={node.user?.shortName ?? node.num.toString()}
|
||||||
|
className={cn(hasNodeError(node.num) && "text-red-500")}
|
||||||
|
showError={hasNodeError(node.num)}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -145,7 +148,6 @@ export const MessagesPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Single message input for both chat types */}
|
|
||||||
<div className="shrink-0 p-4 w-full dark:bg-slate-900">
|
<div className="shrink-0 p-4 w-full dark:bg-slate-900">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
to={messageDestination}
|
to={messageDestination}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ export interface DeleteNoteDialogProps {
|
|||||||
|
|
||||||
const NodesPage = (): JSX.Element => {
|
const NodesPage = (): JSX.Element => {
|
||||||
const { nodes, hardware, connection } = useDevice();
|
const { nodes, hardware, connection } = useDevice();
|
||||||
console.log(connection);
|
|
||||||
|
|
||||||
const [selectedNode, setSelectedNode] = useState<
|
const [selectedNode, setSelectedNode] = useState<
|
||||||
Protobuf.Mesh.NodeInfo | undefined
|
Protobuf.Mesh.NodeInfo | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|||||||
+3
-3
@@ -44,9 +44,9 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
headers: {
|
headers: {
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ['react-scan']
|
exclude: ['react-scan']
|
||||||
|
|||||||
Reference in New Issue
Block a user