Merge pull request #516 from danditomaso/feat/add-key-mismatch-error-handling
feat: add error handling for key mismatch
This commit is contained in:
@@ -15,9 +15,10 @@ const EmptyState = () => (
|
||||
);
|
||||
|
||||
export const ChannelChat = ({
|
||||
messages,
|
||||
messages = [],
|
||||
}: ChannelChatProps) => {
|
||||
const { nodes } = useDevice();
|
||||
|
||||
const messagesEndRef = 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"
|
||||
>
|
||||
<div className="flex flex-col justify-end min-h-full">
|
||||
{messages.map((message, index) => (
|
||||
{messages?.map((message, index) => (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo, useMemo } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
@@ -12,15 +13,13 @@ import {
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { AlertCircle, CheckCircle2, CircleEllipsis, LucideIcon } from "lucide-react";
|
||||
|
||||
const MESSAGE_STATES = {
|
||||
ACK: "ack",
|
||||
WAITING: "waiting",
|
||||
FAILED: "failed",
|
||||
} as const;
|
||||
type MessageStateValue = {
|
||||
state: string;
|
||||
icon: LucideIcon;
|
||||
displayText: string;
|
||||
}
|
||||
|
||||
type MessageState = MessageWithState["state"];
|
||||
|
||||
@@ -40,31 +39,36 @@ interface StatusIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STATUS_TEXT_MAP: Record<MessageState, string> = {
|
||||
[MESSAGE_STATES.ACK]: "Message delivered",
|
||||
[MESSAGE_STATES.WAITING]: "Waiting for delivery",
|
||||
[MESSAGE_STATES.FAILED]: "Delivery failed",
|
||||
const MESSAGE_STATES: Record<string, MessageStateValue> = {
|
||||
ACK: { state: 'ack', icon: CheckCircle2, displayText: "Message delivered" },
|
||||
WAITING: { state: 'waiting', icon: CircleEllipsis, displayText: "Waiting for delivery" },
|
||||
FAILED: { state: 'failed', icon: AlertCircle, displayText: "Delivery failed" },
|
||||
};
|
||||
|
||||
const STATUS_ICON_MAP: Record<MessageState, LucideIcon> = {
|
||||
[MESSAGE_STATES.ACK]: CheckCircle2,
|
||||
[MESSAGE_STATES.WAITING]: CircleEllipsis,
|
||||
[MESSAGE_STATES.FAILED]: AlertCircle,
|
||||
};
|
||||
|
||||
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
|
||||
const getMessageState = (state: MessageState): MessageStateValue => {
|
||||
switch (state) {
|
||||
case MESSAGE_STATES.ACK.state:
|
||||
return MESSAGE_STATES.ACK;
|
||||
case MESSAGE_STATES.WAITING.state:
|
||||
return MESSAGE_STATES.WAITING;
|
||||
case MESSAGE_STATES.FAILED.state:
|
||||
return MESSAGE_STATES.FAILED;
|
||||
default:
|
||||
return MESSAGE_STATES.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
||||
<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"
|
||||
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"
|
||||
align="center"
|
||||
sideOffset={5}
|
||||
>
|
||||
{getStatusText(state)}
|
||||
{getMessageState(state).displayText ?? "An unknown error occurred"};
|
||||
<TooltipArrow className="fill-slate-800" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -72,13 +76,17 @@ const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
||||
);
|
||||
|
||||
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
||||
const msgState = getMessageState(state);
|
||||
|
||||
const isFailed = msgState.state === 'failed'
|
||||
|
||||
const iconClass = cn(
|
||||
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 (
|
||||
<StatusTooltip state={state}>
|
||||
<Icon
|
||||
@@ -90,23 +98,7 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getMessageTextStyles = (state: MessageState) => {
|
||||
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 }) => (
|
||||
const TimeDisplay = memo(({ 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()}
|
||||
@@ -118,9 +110,9 @@ const TimeDisplay = ({
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
));
|
||||
|
||||
export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
const { getDevices } = useDeviceStore();
|
||||
|
||||
const isDeviceUser = useMemo(
|
||||
@@ -128,33 +120,47 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
getDevices()
|
||||
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
||||
.includes(message.from),
|
||||
[getDevices, message.from],
|
||||
[getDevices, message.from]
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col w-full px-4 justify-start">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col flex-wrap items-start py-1",
|
||||
isDeviceUser && "items-end",
|
||||
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?.shortName ?? "UNK"} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||
{messageUser?.longName}
|
||||
</span>
|
||||
</div>
|
||||
{!lastMsgSameUser && (
|
||||
<div className="flex place-items-center gap-2 mb-1">
|
||||
<Avatar text={messageUser?.shortName ?? "UNK"} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||
{messageUser?.longName}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TimeDisplay date={message.rxTime} />
|
||||
<div className="flex place-items-center gap-2 pb-2">
|
||||
@@ -166,4 +172,4 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user