Tidy up message dedupe logic

This commit is contained in:
Jack Kingsman
2026-01-13 19:16:20 -08:00
parent 1eeed67b14
commit f368b80221
6 changed files with 25 additions and 174 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-70eOpM-W.js"></script>
<script type="module" crossorigin src="/assets/index-CfOMUIYi.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DZ67iE5i.css">
</head>
<body>

View File

@@ -174,20 +174,10 @@ export function MessageList({
return <div className="flex-1 overflow-y-auto p-5 text-center text-muted-foreground">No messages yet</div>;
}
// Deduplicate messages by content + timestamp (same message via different paths)
const deduplicatedMessages = messages.reduce<Message[]>((acc, msg) => {
const key = `${msg.type}-${msg.conversation_key}-${msg.text}-${msg.sender_timestamp}`;
const existing = acc.find(m =>
`${m.type}-${m.conversation_key}-${m.text}-${m.sender_timestamp}` === key
);
if (!existing) {
acc.push(msg);
}
return acc;
}, []);
// Sort messages by received_at ascending (oldest first)
const sortedMessages = [...deduplicatedMessages].sort(
// Note: Deduplication is handled by useConversationMessages.addMessageIfNew()
// and the database UNIQUE constraint on (type, conversation_key, text, sender_timestamp)
const sortedMessages = [...messages].sort(
(a, b) => a.received_at - b.received_at
);

View File

@@ -1,139 +0,0 @@
/**
* Tests for message deduplication in MessageList.
*
* Messages arriving via different packet paths should be deduplicated
* based on (type, conversation_key, text, sender_timestamp).
*/
import { describe, it, expect } from 'vitest';
import type { Message } from '../types';
/**
* Deduplication logic extracted from MessageList for testing.
* Same message via different paths = same (type, conversation_key, text, timestamp)
*/
function deduplicateMessages(messages: Message[]): Message[] {
return messages.reduce<Message[]>((acc, msg) => {
const key = `${msg.type}-${msg.conversation_key}-${msg.text}-${msg.sender_timestamp}`;
const existing = acc.find(m =>
`${m.type}-${m.conversation_key}-${m.text}-${m.sender_timestamp}` === key
);
if (!existing) {
acc.push(msg);
}
return acc;
}, []);
}
function createMessage(overrides: Partial<Message>): Message {
return {
id: 1,
type: 'CHAN',
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', // 32-char hex channel key
text: 'Test message',
sender_timestamp: 1700000000,
received_at: 1700000001,
path_len: null,
txt_type: 0,
signature: null,
outgoing: false,
acked: 0,
...overrides,
};
}
describe('Message Deduplication', () => {
it('keeps unique messages', () => {
const messages = [
createMessage({ id: 1, text: 'Message 1', sender_timestamp: 1000 }),
createMessage({ id: 2, text: 'Message 2', sender_timestamp: 2000 }),
createMessage({ id: 3, text: 'Message 3', sender_timestamp: 3000 }),
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(3);
});
it('deduplicates same channel message via different paths', () => {
const messages = [
createMessage({ id: 1, conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', text: 'Hello', sender_timestamp: 1000 }),
createMessage({ id: 2, conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', text: 'Hello', sender_timestamp: 1000 }), // duplicate
createMessage({ id: 3, conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', text: 'Hello', sender_timestamp: 1000 }), // duplicate
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(1);
expect(result[0].id).toBe(1); // keeps first occurrence
});
it('keeps messages with same text but different timestamps', () => {
const messages = [
createMessage({ id: 1, text: 'Hello', sender_timestamp: 1000 }),
createMessage({ id: 2, text: 'Hello', sender_timestamp: 2000 }), // different timestamp
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(2);
});
it('keeps messages with same text but different channels', () => {
const messages = [
createMessage({ id: 1, conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', text: 'Hello', sender_timestamp: 1000 }),
createMessage({ id: 2, conversation_key: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', text: 'Hello', sender_timestamp: 1000 }), // different channel
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(2);
});
it('deduplicates same DM via different paths', () => {
const messages = [
createMessage({ id: 1, type: 'PRIV', conversation_key: 'abc123def456789012345678901234567890123456789012345678901234', text: 'Hi', sender_timestamp: 1000 }),
createMessage({ id: 2, type: 'PRIV', conversation_key: 'abc123def456789012345678901234567890123456789012345678901234', text: 'Hi', sender_timestamp: 1000 }), // duplicate
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(1);
});
it('keeps DMs from different senders with same text', () => {
const messages = [
createMessage({ id: 1, type: 'PRIV', conversation_key: 'abc123def456789012345678901234567890123456789012345678901234', text: 'Hi', sender_timestamp: 1000 }),
createMessage({ id: 2, type: 'PRIV', conversation_key: 'def456789012345678901234567890123456789012345678901234567890', text: 'Hi', sender_timestamp: 1000 }),
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(2);
});
it('keeps channel message and DM with same text', () => {
const messages = [
createMessage({ id: 1, type: 'CHAN', conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', text: 'Hello', sender_timestamp: 1000 }),
createMessage({ id: 2, type: 'PRIV', conversation_key: 'abc123def456789012345678901234567890123456789012345678901234', text: 'Hello', sender_timestamp: 1000 }),
];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(2);
});
it('handles empty array', () => {
const result = deduplicateMessages([]);
expect(result).toHaveLength(0);
});
it('handles single message', () => {
const messages = [createMessage({ id: 1 })];
const result = deduplicateMessages(messages);
expect(result).toHaveLength(1);
});
});