mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-05 13:02:58 +02:00
Fix some misc. frontend correctness bugs
This commit is contained in:
@@ -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]) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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')];
|
||||
|
||||
Reference in New Issue
Block a user