mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Refetch channels on reconnect and fix up count-change refresh guard
This commit is contained in:
@@ -300,6 +300,7 @@ export function App() {
|
||||
// Silently recover any data missed during the disconnect window
|
||||
triggerReconcile();
|
||||
refreshUnreads();
|
||||
api.getChannels().then(setChannels).catch(console.error);
|
||||
fetchAllContacts()
|
||||
.then((data) => setContacts(data))
|
||||
.catch(console.error);
|
||||
|
||||
@@ -88,10 +88,12 @@ export function useUnreadCounts(
|
||||
// On mount, consume the prefetched promise (started in index.html before
|
||||
// React loaded) or fall back to a fresh fetch.
|
||||
// Re-fetch when channel/contact count changes mid-session (new sync, cracker
|
||||
// channel created, etc.) but skip the initial 0→N load to avoid double calls.
|
||||
// channel created, etc.). Skip only the very first run of this effect; after
|
||||
// that, any count change should trigger a refresh, even if the other
|
||||
// collection is still empty.
|
||||
const channelsLen = channels.length;
|
||||
const contactsLen = contacts.length;
|
||||
const prevLens = useRef({ channels: 0, contacts: 0 });
|
||||
const hasObservedCountsRef = useRef(false);
|
||||
useEffect(() => {
|
||||
takePrefetchOrFetch('unreads', api.getUnreads)
|
||||
.then(applyUnreads)
|
||||
@@ -100,10 +102,10 @@ export function useUnreadCounts(
|
||||
});
|
||||
}, [applyUnreads]);
|
||||
useEffect(() => {
|
||||
const prev = prevLens.current;
|
||||
prevLens.current = { channels: channelsLen, contacts: contactsLen };
|
||||
// Skip the initial load (0→N); only refetch on mid-session count changes
|
||||
if (prev.channels === 0 || prev.contacts === 0) return;
|
||||
if (!hasObservedCountsRef.current) {
|
||||
hasObservedCountsRef.current = true;
|
||||
return;
|
||||
}
|
||||
fetchUnreads();
|
||||
}, [channelsLen, contactsLen, fetchUnreads]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -36,9 +36,11 @@ const mocks = vi.hoisted(() => ({
|
||||
fetchOlderMessages: vi.fn(async () => {}),
|
||||
addMessageIfNew: vi.fn(),
|
||||
updateMessageAck: vi.fn(),
|
||||
triggerReconcile: vi.fn(),
|
||||
incrementUnread: vi.fn(),
|
||||
markAllRead: vi.fn(),
|
||||
trackNewMessage: vi.fn(),
|
||||
refreshUnreads: vi.fn(async () => {}),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -59,11 +61,17 @@ vi.mock('../hooks', async (importOriginal) => {
|
||||
messagesLoading: false,
|
||||
loadingOlder: false,
|
||||
hasOlderMessages: false,
|
||||
hasNewerMessages: false,
|
||||
loadingNewer: false,
|
||||
hasNewerMessagesRef: { current: false },
|
||||
setMessages: mocks.hookFns.setMessages,
|
||||
fetchMessages: mocks.hookFns.fetchMessages,
|
||||
fetchOlderMessages: mocks.hookFns.fetchOlderMessages,
|
||||
fetchNewerMessages: vi.fn(async () => {}),
|
||||
jumpToBottom: vi.fn(),
|
||||
addMessageIfNew: mocks.hookFns.addMessageIfNew,
|
||||
updateMessageAck: mocks.hookFns.updateMessageAck,
|
||||
triggerReconcile: mocks.hookFns.triggerReconcile,
|
||||
}),
|
||||
useUnreadCounts: () => ({
|
||||
unreadCounts: {},
|
||||
@@ -72,6 +80,7 @@ vi.mock('../hooks', async (importOriginal) => {
|
||||
incrementUnread: mocks.hookFns.incrementUnread,
|
||||
markAllRead: mocks.hookFns.markAllRead,
|
||||
trackNewMessage: mocks.hookFns.trackNewMessage,
|
||||
refreshUnreads: mocks.hookFns.refreshUnreads,
|
||||
}),
|
||||
getMessageContentKey: () => 'content-key',
|
||||
};
|
||||
@@ -165,6 +174,7 @@ vi.mock('../utils/urlHash', () => ({
|
||||
}));
|
||||
|
||||
import { App } from '../App';
|
||||
import { useWebSocket } from '../useWebSocket';
|
||||
|
||||
const baseConfig = {
|
||||
public_key: 'aa'.repeat(32),
|
||||
@@ -264,6 +274,28 @@ describe('App favorite toggle flow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('re-fetches channels after WebSocket reconnect', async () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.api.getChannels).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const wsHandlers = vi.mocked(useWebSocket).mock.calls[0]?.[0];
|
||||
expect(wsHandlers?.onReconnect).toBeTypeOf('function');
|
||||
|
||||
await act(async () => {
|
||||
wsHandlers?.onReconnect?.();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.api.getChannels).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(mocks.hookFns.triggerReconcile).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.hookFns.refreshUnreads).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toggles settings page mode and syncs selected section into SettingsModal', async () => {
|
||||
render(<App />);
|
||||
|
||||
|
||||
@@ -228,6 +228,62 @@ describe('useUnreadCounts', () => {
|
||||
expect(result.current.unreadCounts[`channel-${CHANNEL_KEY}`]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('re-fetches when channels change while contacts remain empty', async () => {
|
||||
const mocks = await getMockedApi();
|
||||
const initialChannels = [makeChannel(CHANNEL_KEY, 'Test')];
|
||||
const addedChannelKey = '11223344556677889900AABBCCDDEEFF';
|
||||
|
||||
const { rerender } = renderWith({ channels: initialChannels, contacts: [] });
|
||||
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(mocks.getUnreads).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
mocks.getUnreads.mockResolvedValueOnce({
|
||||
counts: { [`channel-${addedChannelKey}`]: 2 },
|
||||
mentions: {},
|
||||
last_message_times: {},
|
||||
});
|
||||
|
||||
rerender({
|
||||
channels: [...initialChannels, makeChannel(addedChannelKey, 'Added')],
|
||||
contacts: [],
|
||||
activeConversation: null,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(mocks.getUnreads).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
});
|
||||
|
||||
it('re-fetches when contacts change while channels remain empty', async () => {
|
||||
const mocks = await getMockedApi();
|
||||
const initialContact = makeContact(CONTACT_KEY);
|
||||
const addedContactKey = 'ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100';
|
||||
|
||||
const { rerender } = renderWith({ channels: [], contacts: [initialContact] });
|
||||
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(mocks.getUnreads).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
mocks.getUnreads.mockResolvedValueOnce({
|
||||
counts: { [`contact-${addedContactKey}`]: 1 },
|
||||
mentions: {},
|
||||
last_message_times: {},
|
||||
});
|
||||
|
||||
rerender({
|
||||
channels: [],
|
||||
contacts: [initialContact, makeContact(addedContactKey)],
|
||||
activeConversation: null,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await vi.waitFor(() => expect(mocks.getUnreads).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
});
|
||||
|
||||
it('does not filter when no active conversation', async () => {
|
||||
const mocks = await getMockedApi();
|
||||
mocks.getUnreads.mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user