mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-21 09:02:57 +02:00
Brief interlude -- fix corrupt packet message display
This commit is contained in:
@@ -6,6 +6,35 @@ interface ContactAvatarProps {
|
|||||||
size?: number;
|
size?: number;
|
||||||
contactType?: number;
|
contactType?: number;
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
|
variant?: 'default' | 'corrupt';
|
||||||
|
}
|
||||||
|
|
||||||
|
function CorruptAvatarGraphic({ size }: { size: number }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
data-testid="corrupt-avatar"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 56"
|
||||||
|
width={Math.round(size * 0.6)}
|
||||||
|
height={Math.round(size * 0.94)}
|
||||||
|
shapeRendering="crispEdges"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path fill="#F8E8F8" d="M8 0v24H0v32h24V0" />
|
||||||
|
<path
|
||||||
|
fill="#807098"
|
||||||
|
d="M12 0h2v1h1v1h1v1h-1v1h-2V3h-1V0zm3 0h1v1h-1V0zm2 0h2v1h-1v1h-1V0zm4 0h2v1h-2V0zm-2 1h2v1h-2V1zm4 0h1v3h-2v1h-1V3h1V2h1V1zm-6 2h1v2h-1V3zm2 0h1v2h-1V3zM9 4h4v1H9V4zm5 1h3v4h-1V6h-2V5zm4 0h1v1h-1V5zm2 0h1v1h1V5h2v1h-1v2h-1v1h-2v1h-1V9h-1V7h1V6h1V5zM8 7h1v1H8V7zm0 3h2v1h1v1H9v-1H8v-1zm13 0h1v1h1v1h-2v-2zm2 0h1v1h-1v-1zm-11 1h1v1h-1v-1zm2 0h1v1h-1v-1zm3 0h1v1h-1v-1zm2 0h1v1h-1v-1zM8 12h1v1H8v-1zm3 0h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h2v1h1v1h-2v-1h-3v-1h1v-1zm-5 1h1v1h-1v-1zm2 0h1v1h-1v-1zm7 0h2v1h-2v-1zm3 0h2v1h-2v-1zM8 15h1v1H8v-1zm11 0h2v1h1v1h-2v-1h-1v-1zm4 0h1v1h-1v-1zm-13 1h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h4v1h-4v-1zm-2 2h1v1h-1v-1zm3 0h1v1h1v1h-3v-1h1v-1zm4 0h4v1h-2v1h-3v-1h1v-1zM9 19h1v1H9v-1zm2 0h1v1h-1v-1zm10 1h3v2h-1v-1h-2v-1zm-11 1h2v2H9v-1h1v-1zm4 0h2v3h1v-1h1v2h1v1h-2v-1h-2v-1h-1v-1h-1v-1h1v-1zm6 0h1v1h-1v-1zm-8 2h1v1h-1v-1zm7 0h2v1h-1v1h-1v-2zm4 0h1v1h-1v-1zM0 24h8v3H7v2h3v1H8v1H7v-1H1v-1h5v-4H0v-1zm9 0h3v1H9v-1zm4 0h1v1h-1v-1zm8 0h1v1h-1v-1zm-1 1h1v1h-1v-1zm2 0h2v3h-1v-2h-1v-1zm-6 1h1v2h-1v-2zm3 0h1v2h-1v-2zm2 0h1v2h-1v-2zm-11 2h2v1h1v1h-2v-1h-1v-1zm7 0h2v1h-2v-1zm3 0h1v1h-1v-1zm2 0h1v1h-1v-1zm-6 1h1v2h-1v-2zm3 0h1v1h1v-1h1v2h1v1h-1v2h-1v-1h-1v1h-1v-1h-2v-1h2v-1h-1v-1h1v-1zm4 0h1v1h-1v-1zm-12 2h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h-1v-1zM5 32h1v1H5v-1zm3 1h2v1H8v-1zm3 0h1v1h-1v-1zm3 0h1v1h-1v-1zm9 0h1v1h-1v-1zM6 34h2v1H7v1H6v1h1v1H1v-1H0v-1h1v-1h1v1h1v-1h1v1h1v-1h1v-1zm11 0h2v1h-1v1h1v-1h1v1h1v1h-5v1h-1v-2h2v-2zm5 0h1v1h-1v-1zM8 35h4v1h1v2h-3v-1H7v-1h1v-1zm5 0h2v1h-2v-1zm9 1h1v1h-1v-1zm-1 1h1v1h-1v-1zM2 39h1v2H2v-2zm2 0h1v2H4v-2zm2 0h1v2H6v-2zm2 0h2v2H9v-1H8v-1zm3 0h1v1h-1v-1zm4 0h1v1h-1v-1zm7 0h1v1h-1v-1zm-5 1h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h-1v-1zM1 41h1v1H1v-1zm2 0h1v1H3v-1zm2 0h1v1H5v-1zm2 0h1v1H7v-1zm3 0h2v2h-1v-1h-1v-1zm4 0h2v1h7v1h-1v1h-1v-1h-1v3h-4v-1h1v-2h-2v-1h-1v-1zm9 0h1v1h-1v-1zm-10 2h2v1h1v1h-4v1h1v1h1v-1h1v1h1v1h-4v-1h-1v-3h2v-1zm10 0h1v1h-1v-1zM0 45h10v3H8v-2H0v-1zm22 0h1v1h-1v-1zm-1 1h1v1h-1v-1zm2 0h1v2h-1v-2zM4 48h4v2H7v-1H6v1H5v-1H4v-1zm6 0h2v3h-1v-1h-1v-2zm6 0h7v1h-7v-1zM0 49h4v1h1v1H0v-2zm13 0h1v1h-1v-1zm2 0h1v1h2v1h-3v-2zm-9 1h1v2H6v-2zm2 0h2v1h1v1h1v1H7v-1h1v-2zm11 0h5v1h-1v1h-1v-1h-1v1h-3v-1h1v-1zm-7 1h3v1h-3v-1zM0 52h6v1h1v1H5v-1H4v1h1v1h1v1H4v-1H0v-3zm12 1h1v1h-1v-1zm2 0h1v3h-2v-2h1v-1zm2 0h2v3h-1v-1h-1v-2zm6 0h2v1h-1v1h-2v-1h1v-1zM7 54h1v1H7v-1zm12 0h1v1h-1v-1zM8 55h4v1H8v-1zm15 0h1v1h-1v-1z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#181010"
|
||||||
|
d="M8 0h1v1H8V0zm0 6h1v1H8V6zm2 0h1v1h-1V6zm2 0h1v1h-1V6zm2 0h1v1h-1V6zM8 9h1v1H8V9zm3 0h1v1h-1V9zm2 0h1v1h-1V9zm2 0h1v1h-1V9zm6 0h1v1h-1V9zm2 0h1v1h-1V9zM8 11h1v1H8v-1zm9 1h5v1h-5v-1zm6 0h1v1h-1v-1zm-13 2h4v1h-4v-1zm9 0h4v1h-4v-1zm-4 1h1v1h-1v-1zm2 0h2v1h-2v-1zm-5 2h1v1h-1v-1zm4 0h2v1h-2v-1zm3 0h2v1h-2v-1zm3 0h1v1h-1v-1zM8 18h1v2H8v-2zm1 2h1v1H9v-1zm2 0h2v1h-2v-1zm4 0h1v1h-1v-1zm1 2h4v1h-4v-1zm6 0h2v1h-2v-1zM9 25h1v1H9v-1zm2 0h5v1h-1v1h-1v-1h-1v1h1v1h-2v-1h-1v-2zM0 26h1v1H0v-1zm8 0h1v1h1v1H8v-2zm7 1h1v1h-1v-1zm-8 1h1v1H7v-1zm-7 2h1v1H0v-1zm9 0h2v1H9v-1zm5 0h2v1h-2v-1zM1 31h5v1H1v-1zm6 0h1v1H7v-1zm-7 1h1v2H0v-2zm9 0h7v1H9v-1zm14 0h1v1h-1v-1zM2 33h1v1H2v-1zm2 0h1v1H4v-1zm3 0h1v1H7v-1zm1 1h2v1H8v-1zm4 0h1v1h-1v-1zM0 35h1v1H0v-1zm23 1h1v1h-1v-1zm-1 1h1v2h-1v-2zM2 38h1v1H2v-1zm2 0h1v1H4v-1zm2 0h1v1H6v-1zm2 0h4v1H8v-1zm6 0h1v1h-1v-1zm3 1h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h-1v-1zm2 0h1v1h-1v-1zm-10 1h1v1h-1v-1zm9 0h1v1h-1v-1zM8 42h1v1H8v-1zm10 4h3v2h-2v-1h-1v-1zm-1 1h1v1h-1v-1zm-1 2h1v1h-1v-1zm7 0h1v1h-1v-1zm-5 3h3v1h-3v-1zm4 0h1v1h-1v-1z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#F0B088"
|
||||||
|
d="M8 5h1v1H8V5zm1 1h1v1h1V6h1v1h1V6h1v1h1V6h1v2H9V6zM8 8h1v1H8V8zm1 1h2v1H9V9zm3 0h1v1h-1V9zm2 0h1v1h-1V9zm6 0h1v1h-1V9zm2 0h1v1h-1V9zm-6 3h1v1h-1v-1zm6 0h1v1h-1v-1zM9 14h1v1h4v-1h2v1h-1v1H9v-2zm14 0h1v1h-1v-1zm-7 1h1v1h-1v-1zm-8 1h1v1h3v1H8v-2zm5 1h3v1h-3v-1zm5 0h1v1h-1v-1zm3 0h1v1h-1v-1zm2 0h1v1h-1v-1zM8 20h1v1H8v-1zm2 0h1v1h-1v-1zm3 0h2v1h-2v-1zm-5 2h1v1H8v-1zm12 0h2v1h-2v-1zm-10 3h1v2h1v1h-2v-1H9v-1h1v-1zm3 1h1v1h-1v-1zm2 0h1v1h-1v-1zm-1 1h1v1h-1v-1zM0 29h1v1H0v-1zm11 1h3v1h-3v-1zm-5 1h1v1H6v-1zm-5 2h1v1H1v-1zm2 0h1v1H3v-1zm2 0h2v1H5v-1zm5 0h1v1h1v1h-2v-2zm3 1h3v1h-3v-1zM0 37h1v1H0v-1zm8 0h2v1H8v-1zm15 0h1v1h-1v-1zM1 38h1v1H1v-1zm2 0h1v1H3v-1zm2 0h1v1H5v-1zm2 0h1v1H7v-1zm5 0h2v1h-2v-1zm3 0h7v1h-1v1h-1v-1h-1v1h-1v-1h-1v1h-1v-1h-1v-1zM0 39h1v2H0v-2zm10 0h1v1h-1v-1zm-2 1h1v1H8v-1zm3 0h2v1h-2v-1zm3 0h2v1h-2v-1zm9 0h1v1h-1v-1zm-7 1h7v1h-7v-1zm7 1h1v1h-1v-1zm-3 3h1v1h-1v-1zm-4 1h2v1h-1v1h-1v-2zm2 1h1v1h-1v-1zm-1 2h6v1h-6v-1zm-1 3h2v1h-2v-1zm5 0h1v1h-1v-1zm2 0h1v1h-1v-1z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContactAvatar({
|
export function ContactAvatar({
|
||||||
@@ -14,14 +43,30 @@ export function ContactAvatar({
|
|||||||
size = 28,
|
size = 28,
|
||||||
contactType,
|
contactType,
|
||||||
clickable,
|
clickable,
|
||||||
|
variant = 'default',
|
||||||
}: ContactAvatarProps) {
|
}: ContactAvatarProps) {
|
||||||
|
if (variant === 'corrupt') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center rounded-md flex-shrink-0 select-none bg-black/10${clickable ? ' cursor-pointer' : ''}`}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<CorruptAvatarGraphic size={size} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const avatar = getContactAvatar(name, publicKey, contactType);
|
const avatar = getContactAvatar(name, publicKey, contactType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center rounded-full font-semibold flex-shrink-0 select-none${clickable ? ' cursor-pointer' : ''}`}
|
className={`flex items-center justify-center rounded-full font-semibold flex-shrink-0 select-none${clickable ? ' cursor-pointer' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: avatar.background,
|
background: avatar.background,
|
||||||
color: avatar.textColor,
|
color: avatar.textColor,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
|
|||||||
@@ -148,6 +148,23 @@ function HopCountBadge({ paths, onClick, variant }: HopCountBadgeProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RESEND_WINDOW_SECONDS = 30;
|
const RESEND_WINDOW_SECONDS = 30;
|
||||||
|
const CORRUPT_SENDER_LABEL = '<No name -- corrupt packet?>';
|
||||||
|
|
||||||
|
function hasUnexpectedControlChars(text: string): boolean {
|
||||||
|
for (const char of text) {
|
||||||
|
const code = char.charCodeAt(0);
|
||||||
|
if (
|
||||||
|
(code >= 0 && code <= 8) ||
|
||||||
|
code === 11 ||
|
||||||
|
code === 12 ||
|
||||||
|
(code >= 14 && code <= 31) ||
|
||||||
|
code === 127
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function MessageList({
|
export function MessageList({
|
||||||
messages,
|
messages,
|
||||||
@@ -400,6 +417,17 @@ export function MessageList({
|
|||||||
return contacts.find((c) => c.name === name) || null;
|
return contacts.find((c) => c.name === name) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCorruptUnnamedChannelMessage = (msg: Message, parsedSender: string | null): boolean => {
|
||||||
|
return (
|
||||||
|
msg.type === 'CHAN' &&
|
||||||
|
!msg.outgoing &&
|
||||||
|
!msg.sender_name &&
|
||||||
|
!msg.sender_key &&
|
||||||
|
!parsedSender &&
|
||||||
|
hasUnexpectedControlChars(msg.text)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Build sender info for path modal
|
// Build sender info for path modal
|
||||||
const getSenderInfo = (
|
const getSenderInfo = (
|
||||||
msg: Message,
|
msg: Message,
|
||||||
@@ -415,6 +443,32 @@ export function MessageList({
|
|||||||
pathHashMode: contact.out_path_hash_mode,
|
pathHashMode: contact.out_path_hash_mode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'CHAN') {
|
||||||
|
const senderName = msg.sender_name || parsedSender;
|
||||||
|
const senderContact =
|
||||||
|
(msg.sender_key
|
||||||
|
? contacts.find((candidate) => candidate.public_key === msg.sender_key)
|
||||||
|
: null) || (senderName ? getContactByName(senderName) : null);
|
||||||
|
if (senderContact) {
|
||||||
|
return {
|
||||||
|
name: senderContact.name || senderName || senderContact.public_key.slice(0, 12),
|
||||||
|
publicKeyOrPrefix: senderContact.public_key,
|
||||||
|
lat: senderContact.lat,
|
||||||
|
lon: senderContact.lon,
|
||||||
|
pathHashMode: senderContact.out_path_hash_mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (senderName || msg.sender_key) {
|
||||||
|
return {
|
||||||
|
name: senderName || msg.sender_key || 'Unknown',
|
||||||
|
publicKeyOrPrefix: msg.sender_key || msg.conversation_key || '',
|
||||||
|
lat: null,
|
||||||
|
lon: null,
|
||||||
|
pathHashMode: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For channel messages, try to find contact by parsed sender name
|
// For channel messages, try to find contact by parsed sender name
|
||||||
if (parsedSender) {
|
if (parsedSender) {
|
||||||
const senderContact = getContactByName(parsedSender);
|
const senderContact = getContactByName(parsedSender);
|
||||||
@@ -455,10 +509,17 @@ export function MessageList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get a unique sender key for grouping messages
|
// Helper to get a unique sender key for grouping messages
|
||||||
const getSenderKey = (msg: Message, sender: string | null): string => {
|
const getSenderKey = (
|
||||||
|
msg: Message,
|
||||||
|
senderName: string | null,
|
||||||
|
isCorruptChannelMessage: boolean
|
||||||
|
): string => {
|
||||||
if (msg.outgoing) return '__outgoing__';
|
if (msg.outgoing) return '__outgoing__';
|
||||||
if (msg.type === 'PRIV' && msg.conversation_key) return msg.conversation_key;
|
if (msg.type === 'PRIV' && msg.conversation_key) return msg.conversation_key;
|
||||||
return sender || '__unknown__';
|
if (msg.sender_key) return `key:${msg.sender_key}`;
|
||||||
|
if (senderName) return `name:${senderName}`;
|
||||||
|
if (isCorruptChannelMessage) return `corrupt:${msg.id}`;
|
||||||
|
return '__unknown__';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -487,17 +548,36 @@ export function MessageList({
|
|||||||
const { sender, content } = isRepeater
|
const { sender, content } = isRepeater
|
||||||
? { sender: null, content: msg.text }
|
? { sender: null, content: msg.text }
|
||||||
: parseSenderFromText(msg.text);
|
: parseSenderFromText(msg.text);
|
||||||
|
const channelSenderName = msg.type === 'CHAN' ? msg.sender_name || sender : null;
|
||||||
|
const channelSenderContact =
|
||||||
|
msg.type === 'CHAN' && channelSenderName ? getContactByName(channelSenderName) : null;
|
||||||
|
const isCorruptChannelMessage = isCorruptUnnamedChannelMessage(msg, sender);
|
||||||
const displaySender = msg.outgoing
|
const displaySender = msg.outgoing
|
||||||
? 'You'
|
? 'You'
|
||||||
: contact?.name || sender || msg.conversation_key?.slice(0, 8) || 'Unknown';
|
: contact?.name ||
|
||||||
|
channelSenderName ||
|
||||||
|
(isCorruptChannelMessage
|
||||||
|
? CORRUPT_SENDER_LABEL
|
||||||
|
: msg.conversation_key?.slice(0, 8) || 'Unknown');
|
||||||
|
|
||||||
const canClickSender = !msg.outgoing && onSenderClick && displaySender !== 'Unknown';
|
const canClickSender =
|
||||||
|
!msg.outgoing &&
|
||||||
|
onSenderClick &&
|
||||||
|
displaySender !== 'Unknown' &&
|
||||||
|
displaySender !== CORRUPT_SENDER_LABEL;
|
||||||
|
|
||||||
// Determine if we should show avatar (first message in a chunk from same sender)
|
// Determine if we should show avatar (first message in a chunk from same sender)
|
||||||
const currentSenderKey = getSenderKey(msg, sender);
|
const currentSenderKey = getSenderKey(msg, channelSenderName, isCorruptChannelMessage);
|
||||||
const prevMsg = sortedMessages[index - 1];
|
const prevMsg = sortedMessages[index - 1];
|
||||||
|
const prevParsedSender = prevMsg ? parseSenderFromText(prevMsg.text).sender : null;
|
||||||
const prevSenderKey = prevMsg
|
const prevSenderKey = prevMsg
|
||||||
? getSenderKey(prevMsg, parseSenderFromText(prevMsg.text).sender)
|
? getSenderKey(
|
||||||
|
prevMsg,
|
||||||
|
prevMsg.type === 'CHAN'
|
||||||
|
? prevMsg.sender_name || prevParsedSender
|
||||||
|
: prevParsedSender,
|
||||||
|
isCorruptUnnamedChannelMessage(prevMsg, prevParsedSender)
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
const isFirstInGroup = currentSenderKey !== prevSenderKey;
|
const isFirstInGroup = currentSenderKey !== prevSenderKey;
|
||||||
const showAvatar = !msg.outgoing && isFirstInGroup;
|
const showAvatar = !msg.outgoing && isFirstInGroup;
|
||||||
@@ -506,16 +586,24 @@ export function MessageList({
|
|||||||
// Get avatar info for incoming messages
|
// Get avatar info for incoming messages
|
||||||
let avatarName: string | null = null;
|
let avatarName: string | null = null;
|
||||||
let avatarKey: string = '';
|
let avatarKey: string = '';
|
||||||
|
let avatarVariant: 'default' | 'corrupt' = 'default';
|
||||||
if (!msg.outgoing) {
|
if (!msg.outgoing) {
|
||||||
if (msg.type === 'PRIV' && msg.conversation_key) {
|
if (msg.type === 'PRIV' && msg.conversation_key) {
|
||||||
// DM: use conversation_key (sender's public key)
|
// DM: use conversation_key (sender's public key)
|
||||||
avatarName = contact?.name || null;
|
avatarName = contact?.name || null;
|
||||||
avatarKey = msg.conversation_key;
|
avatarKey = msg.conversation_key;
|
||||||
} else if (sender) {
|
} else if (isCorruptChannelMessage) {
|
||||||
// Channel message: try to find contact by name, or use sender name as pseudo-key
|
avatarName = CORRUPT_SENDER_LABEL;
|
||||||
const senderContact = getContactByName(sender);
|
avatarKey = `corrupt:${msg.id}`;
|
||||||
avatarName = sender;
|
avatarVariant = 'corrupt';
|
||||||
avatarKey = senderContact?.public_key || `name:${sender}`;
|
} else {
|
||||||
|
// Channel message: use stored sender identity first, then parsed/fallback display name
|
||||||
|
avatarName =
|
||||||
|
channelSenderName || (displaySender !== 'Unknown' ? displaySender : null);
|
||||||
|
avatarKey =
|
||||||
|
msg.sender_key ||
|
||||||
|
channelSenderContact?.public_key ||
|
||||||
|
(avatarName ? `name:${avatarName}` : `message:${msg.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +635,7 @@ export function MessageList({
|
|||||||
publicKey={avatarKey}
|
publicKey={avatarKey}
|
||||||
size={32}
|
size={32}
|
||||||
clickable={!!onOpenContactInfo}
|
clickable={!!onOpenContactInfo}
|
||||||
|
variant={avatarVariant}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user