diff --git a/frontend/src/components/ContactAvatar.tsx b/frontend/src/components/ContactAvatar.tsx
index e5cff78..d41fbf6 100644
--- a/frontend/src/components/ContactAvatar.tsx
+++ b/frontend/src/components/ContactAvatar.tsx
@@ -6,6 +6,35 @@ interface ContactAvatarProps {
size?: number;
contactType?: number;
clickable?: boolean;
+ variant?: 'default' | 'corrupt';
+}
+
+function CorruptAvatarGraphic({ size }: { size: number }) {
+ return (
+
+ );
}
export function ContactAvatar({
@@ -14,14 +43,30 @@ export function ContactAvatar({
size = 28,
contactType,
clickable,
+ variant = 'default',
}: ContactAvatarProps) {
+ if (variant === 'corrupt') {
+ return (
+
+
+
+ );
+ }
+
const avatar = getContactAvatar(name, publicKey, contactType);
return (
= 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}
/>
)}