mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-18 07:16:17 +02:00
Dedupe contacts/sidebar by key not name
This commit is contained in:
@@ -140,11 +140,12 @@ export function Sidebar({
|
||||
[lastMessageTimes]
|
||||
);
|
||||
|
||||
// Deduplicate channels by name, keeping the first (lowest index)
|
||||
// Deduplicate channels by key only.
|
||||
// Channel names are not unique; distinct keys must remain visible.
|
||||
const uniqueChannels = useMemo(
|
||||
() =>
|
||||
channels.reduce<Channel[]>((acc, channel) => {
|
||||
if (!acc.some((c) => c.name === channel.name)) {
|
||||
if (!acc.some((c) => c.key === channel.key)) {
|
||||
acc.push(channel);
|
||||
}
|
||||
return acc;
|
||||
|
||||
@@ -100,21 +100,24 @@ export function useConversationMessages(
|
||||
hasOlderMessagesRef.current = hasOlderMessages;
|
||||
}, [hasOlderMessages]);
|
||||
|
||||
const setPendingAck = useCallback((messageId: number, ackCount: number, paths?: MessagePath[]) => {
|
||||
const existing = pendingAcksRef.current.get(messageId);
|
||||
const merged = mergePendingAck(existing, ackCount, paths);
|
||||
const setPendingAck = useCallback(
|
||||
(messageId: number, ackCount: number, paths?: MessagePath[]) => {
|
||||
const existing = pendingAcksRef.current.get(messageId);
|
||||
const merged = mergePendingAck(existing, ackCount, paths);
|
||||
|
||||
// Update insertion order so most recent updates remain in the buffer longest.
|
||||
pendingAcksRef.current.delete(messageId);
|
||||
pendingAcksRef.current.set(messageId, merged);
|
||||
// Update insertion order so most recent updates remain in the buffer longest.
|
||||
pendingAcksRef.current.delete(messageId);
|
||||
pendingAcksRef.current.set(messageId, merged);
|
||||
|
||||
if (pendingAcksRef.current.size > MAX_PENDING_ACKS) {
|
||||
const oldestMessageId = pendingAcksRef.current.keys().next().value as number | undefined;
|
||||
if (oldestMessageId !== undefined) {
|
||||
pendingAcksRef.current.delete(oldestMessageId);
|
||||
if (pendingAcksRef.current.size > MAX_PENDING_ACKS) {
|
||||
const oldestMessageId = pendingAcksRef.current.keys().next().value as number | undefined;
|
||||
if (oldestMessageId !== undefined) {
|
||||
pendingAcksRef.current.delete(oldestMessageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const applyPendingAck = useCallback((msg: Message): Message => {
|
||||
const pending = pendingAcksRef.current.get(msg.id);
|
||||
@@ -345,30 +348,33 @@ export function useConversationMessages(
|
||||
|
||||
// Add a message if it's new (deduplication)
|
||||
// Returns true if the message was added, false if it was a duplicate
|
||||
const addMessageIfNew = useCallback((msg: Message): boolean => {
|
||||
const msgWithPendingAck = applyPendingAck(msg);
|
||||
const contentKey = getMessageContentKey(msgWithPendingAck);
|
||||
if (seenMessageContent.current.has(contentKey)) {
|
||||
console.debug('Duplicate message content ignored:', contentKey.slice(0, 50));
|
||||
return false;
|
||||
}
|
||||
seenMessageContent.current.add(contentKey);
|
||||
|
||||
// Limit set size to prevent memory issues (keep last 500)
|
||||
if (seenMessageContent.current.size > 1000) {
|
||||
const entries = Array.from(seenMessageContent.current);
|
||||
seenMessageContent.current = new Set(entries.slice(-500));
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
if (prev.some((m) => m.id === msgWithPendingAck.id)) {
|
||||
return prev;
|
||||
const addMessageIfNew = useCallback(
|
||||
(msg: Message): boolean => {
|
||||
const msgWithPendingAck = applyPendingAck(msg);
|
||||
const contentKey = getMessageContentKey(msgWithPendingAck);
|
||||
if (seenMessageContent.current.has(contentKey)) {
|
||||
console.debug('Duplicate message content ignored:', contentKey.slice(0, 50));
|
||||
return false;
|
||||
}
|
||||
return [...prev, msgWithPendingAck];
|
||||
});
|
||||
seenMessageContent.current.add(contentKey);
|
||||
|
||||
return true;
|
||||
}, [applyPendingAck]);
|
||||
// Limit set size to prevent memory issues (keep last 500)
|
||||
if (seenMessageContent.current.size > 1000) {
|
||||
const entries = Array.from(seenMessageContent.current);
|
||||
seenMessageContent.current = new Set(entries.slice(-500));
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
if (prev.some((m) => m.id === msgWithPendingAck.id)) {
|
||||
return prev;
|
||||
}
|
||||
return [...prev, msgWithPendingAck];
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
[applyPendingAck]
|
||||
);
|
||||
|
||||
// Update a message's ack count and paths
|
||||
const updateMessageAck = useCallback(
|
||||
@@ -388,7 +394,9 @@ export function useConversationMessages(
|
||||
const current = prev[idx];
|
||||
const nextAck = Math.max(current.acked, ackCount);
|
||||
const nextPaths =
|
||||
paths !== undefined && paths.length >= (current.paths?.length ?? 0) ? paths : current.paths;
|
||||
paths !== undefined && paths.length >= (current.paths?.length ?? 0)
|
||||
? paths
|
||||
: current.paths;
|
||||
|
||||
const updated = [...prev];
|
||||
updated[idx] = {
|
||||
|
||||
@@ -37,6 +37,7 @@ function renderSidebar(overrides?: {
|
||||
unreadCounts?: Record<string, number>;
|
||||
favorites?: Favorite[];
|
||||
lastMessageTimes?: ConversationTimes;
|
||||
channels?: Channel[];
|
||||
}) {
|
||||
const aliceName = 'Alice';
|
||||
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
||||
@@ -53,11 +54,12 @@ function renderSidebar(overrides?: {
|
||||
};
|
||||
|
||||
const favorites = overrides?.favorites ?? [{ type: 'channel', id: flightChannel.key }];
|
||||
const channels = overrides?.channels ?? [publicChannel, flightChannel, opsChannel];
|
||||
|
||||
const view = render(
|
||||
<Sidebar
|
||||
contacts={[alice, relay]}
|
||||
channels={[publicChannel, flightChannel, opsChannel]}
|
||||
channels={channels}
|
||||
activeConversation={null}
|
||||
onSelectConversation={vi.fn()}
|
||||
onNewMessage={vi.fn()}
|
||||
@@ -137,4 +139,40 @@ describe('Sidebar section summaries', () => {
|
||||
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders same-name channels when keys differ and allows selecting both', () => {
|
||||
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
||||
const channelA = makeChannel('DD'.repeat(16), '#shared');
|
||||
const channelB = makeChannel('EE'.repeat(16), '#shared');
|
||||
const onSelectConversation = vi.fn();
|
||||
|
||||
render(
|
||||
<Sidebar
|
||||
contacts={[]}
|
||||
channels={[publicChannel, channelA, channelB]}
|
||||
activeConversation={null}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onNewMessage={vi.fn()}
|
||||
lastMessageTimes={{}}
|
||||
unreadCounts={{}}
|
||||
mentions={{}}
|
||||
showCracker={false}
|
||||
crackerRunning={false}
|
||||
onToggleCracker={vi.fn()}
|
||||
onMarkAllRead={vi.fn()}
|
||||
favorites={[]}
|
||||
sortOrder="recent"
|
||||
onSortOrderChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const sharedRows = screen.getAllByText('#shared');
|
||||
expect(sharedRows).toHaveLength(2);
|
||||
|
||||
fireEvent.click(sharedRows[0]);
|
||||
fireEvent.click(sharedRows[1]);
|
||||
|
||||
const selectedIds = onSelectConversation.mock.calls.map(([conv]) => conv.id);
|
||||
expect(new Set(selectedIds)).toEqual(new Set([channelA.key, channelB.key]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,9 @@ describe('useConversationMessages ACK ordering', () => {
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const added = result.current.addMessageIfNew(createMessage({ id: 42, acked: 0, paths: null }));
|
||||
const added = result.current.addMessageIfNew(
|
||||
createMessage({ id: 42, acked: 0, paths: null })
|
||||
);
|
||||
expect(added).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user