Brief interlude -- fix corrupt packet message display

This commit is contained in:
Jack Kingsman
2026-03-09 20:11:13 -07:00
parent 19d7c3c98c
commit ec5b9663b2
2 changed files with 146 additions and 12 deletions

View File

@@ -6,6 +6,35 @@ interface ContactAvatarProps {
size?: number;
contactType?: number;
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({
@@ -14,14 +43,30 @@ export function ContactAvatar({
size = 28,
contactType,
clickable,
variant = 'default',
}: 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);
return (
<div
className={`flex items-center justify-center rounded-full font-semibold flex-shrink-0 select-none${clickable ? ' cursor-pointer' : ''}`}
style={{
backgroundColor: avatar.background,
background: avatar.background,
color: avatar.textColor,
width: size,
height: size,

View File

@@ -148,6 +148,23 @@ function HopCountBadge({ paths, onClick, variant }: HopCountBadgeProps) {
}
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({
messages,
@@ -400,6 +417,17 @@ export function MessageList({
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
const getSenderInfo = (
msg: Message,
@@ -415,6 +443,32 @@ export function MessageList({
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
if (parsedSender) {
const senderContact = getContactByName(parsedSender);
@@ -455,10 +509,17 @@ export function MessageList({
}
// 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.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 (
@@ -487,17 +548,36 @@ export function MessageList({
const { sender, content } = isRepeater
? { sender: null, content: 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
? '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)
const currentSenderKey = getSenderKey(msg, sender);
const currentSenderKey = getSenderKey(msg, channelSenderName, isCorruptChannelMessage);
const prevMsg = sortedMessages[index - 1];
const prevParsedSender = prevMsg ? parseSenderFromText(prevMsg.text).sender : null;
const prevSenderKey = prevMsg
? getSenderKey(prevMsg, parseSenderFromText(prevMsg.text).sender)
? getSenderKey(
prevMsg,
prevMsg.type === 'CHAN'
? prevMsg.sender_name || prevParsedSender
: prevParsedSender,
isCorruptUnnamedChannelMessage(prevMsg, prevParsedSender)
)
: null;
const isFirstInGroup = currentSenderKey !== prevSenderKey;
const showAvatar = !msg.outgoing && isFirstInGroup;
@@ -506,16 +586,24 @@ export function MessageList({
// Get avatar info for incoming messages
let avatarName: string | null = null;
let avatarKey: string = '';
let avatarVariant: 'default' | 'corrupt' = 'default';
if (!msg.outgoing) {
if (msg.type === 'PRIV' && msg.conversation_key) {
// DM: use conversation_key (sender's public key)
avatarName = contact?.name || null;
avatarKey = msg.conversation_key;
} else if (sender) {
// Channel message: try to find contact by name, or use sender name as pseudo-key
const senderContact = getContactByName(sender);
avatarName = sender;
avatarKey = senderContact?.public_key || `name:${sender}`;
} else if (isCorruptChannelMessage) {
avatarName = CORRUPT_SENDER_LABEL;
avatarKey = `corrupt:${msg.id}`;
avatarVariant = 'corrupt';
} 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}
size={32}
clickable={!!onOpenContactInfo}
variant={avatarVariant}
/>
</span>
)}