Fix some misc. frontend correctness bugs

This commit is contained in:
Jack Kingsman
2026-03-30 21:29:01 -07:00
parent 7b9d8f6a23
commit 3c0d6a4466
4 changed files with 82 additions and 19 deletions

View File

@@ -131,9 +131,23 @@ export function MapView({ contacts, focusedKey }: MapViewProps) {
// Store ref for a marker
const setMarkerRef = useCallback((key: string, ref: LeafletCircleMarker | null) => {
if (ref === null) {
delete markerRefs.current[key];
return;
}
markerRefs.current[key] = ref;
}, []);
useEffect(() => {
const currentKeys = new Set(mappableContacts.map((contact) => contact.public_key));
for (const key of Object.keys(markerRefs.current)) {
if (!currentKeys.has(key)) {
delete markerRefs.current[key];
}
}
}, [mappableContacts]);
// Open popup for focused contact after map is ready
useEffect(() => {
if (focusedContact && markerRefs.current[focusedContact.public_key]) {

View File

@@ -373,7 +373,22 @@ export function MessageList({
}
}
setResendableIds(newResendable);
setResendableIds((prev) => {
if (prev.size === newResendable.size) {
let changed = false;
for (const id of newResendable) {
if (!prev.has(id)) {
changed = true;
break;
}
}
if (!changed) {
return prev;
}
}
return newResendable;
});
return () => {
for (const timer of timers.values()) clearTimeout(timer);

View File

@@ -10,6 +10,12 @@ import {
import type { Channel, Contact, Conversation, Message, UnreadCounts } from '../types';
import { takePrefetchOrFetch } from '../prefetch';
function isUnreadTrackedConversation(
conversation: Conversation | null
): conversation is Extract<Conversation, { type: 'channel' | 'contact' }> {
return conversation?.type === 'channel' || conversation?.type === 'contact';
}
interface UseUnreadCountsResult {
unreadCounts: Record<string, number>;
/** Tracks which conversations have unread messages that mention the user */
@@ -48,14 +54,7 @@ export function useUnreadCounts(
// (the user is already viewing it, so its count should stay at 0).
const applyUnreads = useCallback((data: UnreadCounts) => {
const ac = activeConvRef.current;
const activeKey =
ac &&
ac.type !== 'raw' &&
ac.type !== 'map' &&
ac.type !== 'visualizer' &&
ac.type !== 'search'
? getStateKey(ac.type as 'channel' | 'contact', ac.id)
: null;
const activeKey = isUnreadTrackedConversation(ac) ? getStateKey(ac.type, ac.id) : null;
if (activeKey) {
const counts = { ...data.counts };
@@ -123,16 +122,8 @@ export function useUnreadCounts(
// Mark conversation as read when user views it
// Calls server API to persist read state across devices
useEffect(() => {
if (
activeConversation &&
activeConversation.type !== 'raw' &&
activeConversation.type !== 'map' &&
activeConversation.type !== 'visualizer'
) {
const key = getStateKey(
activeConversation.type as 'channel' | 'contact',
activeConversation.id
);
if (isUnreadTrackedConversation(activeConversation)) {
const key = getStateKey(activeConversation.type, activeConversation.id);
// Update local state immediately for responsive UI
setUnreadCounts((prev) => {

View File

@@ -221,6 +221,49 @@ describe('useUnreadCounts', () => {
});
});
it('does not treat search or trace views as readable conversations', async () => {
const mocks = await getMockedApi();
mocks.getUnreads.mockResolvedValue({
counts: {
[getStateKey('channel', CHANNEL_KEY)]: 4,
[getStateKey('contact', CONTACT_KEY)]: 2,
},
mentions: {
[getStateKey('channel', CHANNEL_KEY)]: true,
},
last_message_times: {},
last_read_ats: {},
});
const { result, rerender } = renderWith({
channels: [makeChannel(CHANNEL_KEY, 'Test')],
contacts: [makeContact(CONTACT_KEY)],
activeConversation: { type: 'search', id: 'search', name: 'Message Search' },
});
await act(async () => {
await vi.waitFor(() => expect(mocks.getUnreads).toHaveBeenCalled());
});
expect(result.current.unreadCounts[getStateKey('channel', CHANNEL_KEY)]).toBe(4);
expect(result.current.unreadCounts[getStateKey('contact', CONTACT_KEY)]).toBe(2);
expect(mocks.markChannelRead).not.toHaveBeenCalled();
expect(mocks.markContactRead).not.toHaveBeenCalled();
rerender({
channels: [makeChannel(CHANNEL_KEY, 'Test')],
contacts: [makeContact(CONTACT_KEY)],
activeConversation: { type: 'trace', id: 'trace', name: 'Trace' },
});
await act(async () => {
await Promise.resolve();
});
expect(mocks.markChannelRead).not.toHaveBeenCalled();
expect(mocks.markContactRead).not.toHaveBeenCalled();
});
it('re-fetches and filters when refreshUnreads is called (simulating WS reconnect)', async () => {
const mocks = await getMockedApi();
const channels = [makeChannel(CHANNEL_KEY, 'Test')];