diff --git a/frontend/prebuilt/assets/BotCodeEditor-3AsJnjQI.js b/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-3AsJnjQI.js rename to frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js index 106a4efb..817145d7 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-3AsJnjQI.js and b/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-3AsJnjQI.js.map b/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js.map similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-3AsJnjQI.js.map rename to frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js.map index fc2315ca..808e44f1 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-3AsJnjQI.js.map and b/frontend/prebuilt/assets/BotCodeEditor-BiRWyRIv.js.map differ diff --git a/frontend/prebuilt/assets/CrackerPanel-i7_kX2h1.js b/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-i7_kX2h1.js rename to frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js index 11253bc9..6e3a52ef 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-i7_kX2h1.js and b/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js differ diff --git a/frontend/prebuilt/assets/CrackerPanel-i7_kX2h1.js.map b/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js.map similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-i7_kX2h1.js.map rename to frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js.map index ce1cab92..e8e997bd 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-i7_kX2h1.js.map and b/frontend/prebuilt/assets/CrackerPanel-BndH8RyA.js.map differ diff --git a/frontend/prebuilt/assets/MapView-8l-XCyfo.js b/frontend/prebuilt/assets/MapView-B_EeV5Ur.js similarity index 92% rename from frontend/prebuilt/assets/MapView-8l-XCyfo.js rename to frontend/prebuilt/assets/MapView-B_EeV5Ur.js index d71301a5..347043ff 100644 Binary files a/frontend/prebuilt/assets/MapView-8l-XCyfo.js and b/frontend/prebuilt/assets/MapView-B_EeV5Ur.js differ diff --git a/frontend/prebuilt/assets/MapView-8l-XCyfo.js.map b/frontend/prebuilt/assets/MapView-B_EeV5Ur.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-8l-XCyfo.js.map rename to frontend/prebuilt/assets/MapView-B_EeV5Ur.js.map index 0b5cb1b0..71be677d 100644 Binary files a/frontend/prebuilt/assets/MapView-8l-XCyfo.js.map and b/frontend/prebuilt/assets/MapView-B_EeV5Ur.js.map differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-CI9vDONH.js b/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-CI9vDONH.js rename to frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js index ab8a88a9..3cb59969 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-CI9vDONH.js and b/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-CI9vDONH.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js.map similarity index 97% rename from frontend/prebuilt/assets/NeighborsMiniMap-CI9vDONH.js.map rename to frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js.map index a3afa658..d1562189 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-CI9vDONH.js.map and b/frontend/prebuilt/assets/NeighborsMiniMap-CsH85D0E.js.map differ diff --git a/frontend/prebuilt/assets/PathRouteMap--4_fo75Z.js b/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js similarity index 95% rename from frontend/prebuilt/assets/PathRouteMap--4_fo75Z.js rename to frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js index 880e8d86..3744db84 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap--4_fo75Z.js and b/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js differ diff --git a/frontend/prebuilt/assets/PathRouteMap--4_fo75Z.js.map b/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap--4_fo75Z.js.map rename to frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js.map index 992d7f27..71173ea5 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap--4_fo75Z.js.map and b/frontend/prebuilt/assets/PathRouteMap-DbzDNGyG.js.map differ diff --git a/frontend/prebuilt/assets/Popup-_4Won2BS.js b/frontend/prebuilt/assets/Popup-BcLg2SsA.js similarity index 81% rename from frontend/prebuilt/assets/Popup-_4Won2BS.js rename to frontend/prebuilt/assets/Popup-BcLg2SsA.js index b1a7a8c0..19c5d90b 100644 Binary files a/frontend/prebuilt/assets/Popup-_4Won2BS.js and b/frontend/prebuilt/assets/Popup-BcLg2SsA.js differ diff --git a/frontend/prebuilt/assets/Popup-_4Won2BS.js.map b/frontend/prebuilt/assets/Popup-BcLg2SsA.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-_4Won2BS.js.map rename to frontend/prebuilt/assets/Popup-BcLg2SsA.js.map index 7db9e019..0ef254ef 100644 Binary files a/frontend/prebuilt/assets/Popup-_4Won2BS.js.map and b/frontend/prebuilt/assets/Popup-BcLg2SsA.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-BmKo2O_g.js b/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js similarity index 98% rename from frontend/prebuilt/assets/RepeaterDashboard-BmKo2O_g.js rename to frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js index de24e1fe..4a2f919d 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-BmKo2O_g.js and b/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-BmKo2O_g.js.map b/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js.map similarity index 99% rename from frontend/prebuilt/assets/RepeaterDashboard-BmKo2O_g.js.map rename to frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js.map index 41e090a0..eaafc690 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-BmKo2O_g.js.map and b/frontend/prebuilt/assets/RepeaterDashboard-DX2bA1gX.js.map differ diff --git a/frontend/prebuilt/assets/SearchView-CvQIQlHy.js b/frontend/prebuilt/assets/SearchView-RhP_4yQI.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-CvQIQlHy.js rename to frontend/prebuilt/assets/SearchView-RhP_4yQI.js index 13ce6ee8..745500be 100644 Binary files a/frontend/prebuilt/assets/SearchView-CvQIQlHy.js and b/frontend/prebuilt/assets/SearchView-RhP_4yQI.js differ diff --git a/frontend/prebuilt/assets/SearchView-CvQIQlHy.js.map b/frontend/prebuilt/assets/SearchView-RhP_4yQI.js.map similarity index 99% rename from frontend/prebuilt/assets/SearchView-CvQIQlHy.js.map rename to frontend/prebuilt/assets/SearchView-RhP_4yQI.js.map index acd02598..92affefe 100644 Binary files a/frontend/prebuilt/assets/SearchView-CvQIQlHy.js.map and b/frontend/prebuilt/assets/SearchView-RhP_4yQI.js.map differ diff --git a/frontend/prebuilt/assets/SettingsModal-CgS3G4ZT.js b/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-CgS3G4ZT.js rename to frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js index 456bdaf5..6869fb78 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-CgS3G4ZT.js and b/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js differ diff --git a/frontend/prebuilt/assets/SettingsModal-CgS3G4ZT.js.map b/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js.map similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-CgS3G4ZT.js.map rename to frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js.map index e4d2fae1..07281cb0 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-CgS3G4ZT.js.map and b/frontend/prebuilt/assets/SettingsModal-BMRZWaZh.js.map differ diff --git a/frontend/prebuilt/assets/VisualizerView-CPkw7P96.js b/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-CPkw7P96.js rename to frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js index b0c22913..af776e07 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-CPkw7P96.js and b/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js differ diff --git a/frontend/prebuilt/assets/VisualizerView-CPkw7P96.js.map b/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-CPkw7P96.js.map rename to frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js.map index 9a0bbaa9..136bb923 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-CPkw7P96.js.map and b/frontend/prebuilt/assets/VisualizerView-BqTF9y_L.js.map differ diff --git a/frontend/prebuilt/assets/hooks-BmAJqTPK.js b/frontend/prebuilt/assets/hooks-BmAJqTPK.js new file mode 100644 index 00000000..4058ac9a Binary files /dev/null and b/frontend/prebuilt/assets/hooks-BmAJqTPK.js differ diff --git a/frontend/prebuilt/assets/hooks-fCOseuFw.js.map b/frontend/prebuilt/assets/hooks-BmAJqTPK.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-fCOseuFw.js.map rename to frontend/prebuilt/assets/hooks-BmAJqTPK.js.map index 08ead036..b98d8243 100644 Binary files a/frontend/prebuilt/assets/hooks-fCOseuFw.js.map and b/frontend/prebuilt/assets/hooks-BmAJqTPK.js.map differ diff --git a/frontend/prebuilt/assets/hooks-fCOseuFw.js b/frontend/prebuilt/assets/hooks-fCOseuFw.js deleted file mode 100644 index 83c7d50b..00000000 Binary files a/frontend/prebuilt/assets/hooks-fCOseuFw.js and /dev/null differ diff --git a/frontend/prebuilt/assets/index-BUPp0i0i.js b/frontend/prebuilt/assets/index-BUPp0i0i.js deleted file mode 100644 index faf93300..00000000 Binary files a/frontend/prebuilt/assets/index-BUPp0i0i.js and /dev/null differ diff --git a/frontend/prebuilt/assets/index-BUPp0i0i.js.map b/frontend/prebuilt/assets/index-BUPp0i0i.js.map deleted file mode 100644 index 583d6dc8..00000000 Binary files a/frontend/prebuilt/assets/index-BUPp0i0i.js.map and /dev/null differ diff --git a/frontend/prebuilt/assets/index-C_33NHxn.js b/frontend/prebuilt/assets/index-C_33NHxn.js new file mode 100644 index 00000000..c7f74c27 Binary files /dev/null and b/frontend/prebuilt/assets/index-C_33NHxn.js differ diff --git a/frontend/prebuilt/assets/index-C_33NHxn.js.map b/frontend/prebuilt/assets/index-C_33NHxn.js.map new file mode 100644 index 00000000..e70e0516 Binary files /dev/null and b/frontend/prebuilt/assets/index-C_33NHxn.js.map differ diff --git a/frontend/prebuilt/assets/leaflet-Nkpzzebg.js b/frontend/prebuilt/assets/leaflet-DNHXPpW7.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-Nkpzzebg.js rename to frontend/prebuilt/assets/leaflet-DNHXPpW7.js index b1b8062d..569781ad 100644 Binary files a/frontend/prebuilt/assets/leaflet-Nkpzzebg.js and b/frontend/prebuilt/assets/leaflet-DNHXPpW7.js differ diff --git a/frontend/prebuilt/assets/leaflet-Nkpzzebg.js.map b/frontend/prebuilt/assets/leaflet-DNHXPpW7.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-Nkpzzebg.js.map rename to frontend/prebuilt/assets/leaflet-DNHXPpW7.js.map index 823c2f32..2ecb582f 100644 Binary files a/frontend/prebuilt/assets/leaflet-Nkpzzebg.js.map and b/frontend/prebuilt/assets/leaflet-DNHXPpW7.js.map differ diff --git a/frontend/prebuilt/assets/separator-C7ZN2sFh.js b/frontend/prebuilt/assets/separator-BtL4plOj.js similarity index 89% rename from frontend/prebuilt/assets/separator-C7ZN2sFh.js rename to frontend/prebuilt/assets/separator-BtL4plOj.js index 38c83022..86f4b38f 100644 Binary files a/frontend/prebuilt/assets/separator-C7ZN2sFh.js and b/frontend/prebuilt/assets/separator-BtL4plOj.js differ diff --git a/frontend/prebuilt/assets/separator-C7ZN2sFh.js.map b/frontend/prebuilt/assets/separator-BtL4plOj.js.map similarity index 98% rename from frontend/prebuilt/assets/separator-C7ZN2sFh.js.map rename to frontend/prebuilt/assets/separator-BtL4plOj.js.map index f647c1ca..b5ea8ebe 100644 Binary files a/frontend/prebuilt/assets/separator-C7ZN2sFh.js.map and b/frontend/prebuilt/assets/separator-BtL4plOj.js.map differ diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html index f17dc365..dbb38492 100644 Binary files a/frontend/prebuilt/index.html and b/frontend/prebuilt/index.html differ diff --git a/frontend/src/hooks/useConversationMessages.ts b/frontend/src/hooks/useConversationMessages.ts index d097ea8d..fdc25783 100644 --- a/frontend/src/hooks/useConversationMessages.ts +++ b/frontend/src/hooks/useConversationMessages.ts @@ -161,6 +161,8 @@ export function useConversationMessages( const [loadingNewer, setLoadingNewer] = useState(false); const abortControllerRef = useRef(null); + const olderAbortControllerRef = useRef(null); + const newerAbortControllerRef = useRef(null); const fetchingConversationIdRef = useRef(null); const latestReconcileRequestIdRef = useRef(0); const messagesRef = useRef([]); @@ -306,14 +308,19 @@ export function useConversationMessages( loadingOlderRef.current = true; setLoadingOlder(true); + const controller = new AbortController(); + olderAbortControllerRef.current = controller; try { - const data = await api.getMessages({ - type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV', - conversation_key: conversationId, - limit: MESSAGE_PAGE_SIZE, - before: oldestMessage.received_at, - before_id: oldestMessage.id, - }); + const data = await api.getMessages( + { + type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV', + conversation_key: conversationId, + limit: MESSAGE_PAGE_SIZE, + before: oldestMessage.received_at, + before_id: oldestMessage.id, + }, + controller.signal + ); if (fetchingConversationIdRef.current !== conversationId) return; @@ -335,11 +342,17 @@ export function useConversationMessages( } setHasOlderMessages(dataWithPendingAck.length >= MESSAGE_PAGE_SIZE); } catch (err) { + if (isAbortError(err)) { + return; + } console.error('Failed to fetch older messages:', err); toast.error('Failed to load older messages', { description: err instanceof Error ? err.message : 'Check your connection', }); } finally { + if (olderAbortControllerRef.current === controller) { + olderAbortControllerRef.current = null; + } loadingOlderRef.current = false; setLoadingOlder(false); } @@ -361,14 +374,19 @@ export function useConversationMessages( if (!newestMessage) return; setLoadingNewer(true); + const controller = new AbortController(); + newerAbortControllerRef.current = controller; try { - const data = await api.getMessages({ - type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV', - conversation_key: conversationId, - limit: MESSAGE_PAGE_SIZE, - after: newestMessage.received_at, - after_id: newestMessage.id, - }); + const data = await api.getMessages( + { + type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV', + conversation_key: conversationId, + limit: MESSAGE_PAGE_SIZE, + after: newestMessage.received_at, + after_id: newestMessage.id, + }, + controller.signal + ); if (fetchingConversationIdRef.current !== conversationId) return; @@ -385,11 +403,17 @@ export function useConversationMessages( } setHasNewerMessages(dataWithPendingAck.length >= MESSAGE_PAGE_SIZE); } catch (err) { + if (isAbortError(err)) { + return; + } console.error('Failed to fetch newer messages:', err); toast.error('Failed to load newer messages', { description: err instanceof Error ? err.message : 'Check your connection', }); } finally { + if (newerAbortControllerRef.current === controller) { + newerAbortControllerRef.current = null; + } setLoadingNewer(false); } }, [activeConversation, applyPendingAck, hasNewerMessages, loadingNewer, messages]); @@ -420,6 +444,14 @@ export function useConversationMessages( if (abortControllerRef.current) { abortControllerRef.current.abort(); } + if (olderAbortControllerRef.current) { + olderAbortControllerRef.current.abort(); + olderAbortControllerRef.current = null; + } + if (newerAbortControllerRef.current) { + newerAbortControllerRef.current.abort(); + newerAbortControllerRef.current = null; + } const prevId = prevConversationIdRef.current; const newId = activeConversation?.id ?? null; diff --git a/frontend/src/test/useConversationMessages.race.test.ts b/frontend/src/test/useConversationMessages.race.test.ts index dbda153c..e67dfb6a 100644 --- a/frontend/src/test/useConversationMessages.race.test.ts +++ b/frontend/src/test/useConversationMessages.race.test.ts @@ -2,20 +2,28 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import * as messageCache from '../messageCache'; +import { api } from '../api'; import { useConversationMessages } from '../hooks/useConversationMessages'; import type { Conversation, Message } from '../types'; -const mockGetMessages = vi.fn<(...args: unknown[]) => Promise>(); +const mockGetMessages = vi.fn(); const mockGetMessagesAround = vi.fn(); vi.mock('../api', () => ({ api: { - getMessages: (...args: unknown[]) => mockGetMessages(...args), + getMessages: (...args: Parameters) => mockGetMessages(...args), getMessagesAround: (...args: unknown[]) => mockGetMessagesAround(...args), }, isAbortError: (err: unknown) => err instanceof DOMException && err.name === 'AbortError', })); +const mockToastError = vi.fn(); +vi.mock('../components/ui/sonner', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})); + function createConversation(): Conversation { return { type: 'contact', @@ -55,6 +63,7 @@ describe('useConversationMessages ACK ordering', () => { beforeEach(() => { mockGetMessages.mockReset(); messageCache.clear(); + mockToastError.mockReset(); }); it('applies buffered ACK when message is added after ACK event', async () => { @@ -447,6 +456,55 @@ describe('useConversationMessages older-page dedup and reentry', () => { expect(result.current.messages.filter((msg) => msg.id === 0)).toHaveLength(1); expect(result.current.messages).toHaveLength(201); }); + + it('aborts stale older-page requests on conversation switch without toasting', async () => { + const convA: Conversation = { type: 'contact', id: 'conv_a', name: 'Contact A' }; + const convB: Conversation = { type: 'contact', id: 'conv_b', name: 'Contact B' }; + + const fullPage = Array.from({ length: 200 }, (_, i) => + createMessage({ + id: i + 1, + conversation_key: 'conv_a', + text: `msg-${i + 1}`, + sender_timestamp: 1700000000 + i, + received_at: 1700000000 + i, + }) + ); + mockGetMessages.mockResolvedValueOnce(fullPage); + + const olderDeferred = createDeferred(); + let olderSignal: AbortSignal | undefined; + mockGetMessages.mockImplementationOnce((_, signal?: AbortSignal) => { + olderSignal = signal; + signal?.addEventListener('abort', () => { + olderDeferred.resolve([]); + }); + return new Promise((_, reject) => { + signal?.addEventListener('abort', () => { + reject(new DOMException('The operation was aborted', 'AbortError')); + }); + }); + }); + + const { result, rerender } = renderHook( + ({ conv }: { conv: Conversation }) => useConversationMessages(conv), + { initialProps: { conv: convA } } + ); + + await waitFor(() => expect(result.current.messagesLoading).toBe(false)); + act(() => { + void result.current.fetchOlderMessages(); + }); + + await waitFor(() => expect(result.current.loadingOlder).toBe(true)); + + mockGetMessages.mockResolvedValueOnce([createMessage({ id: 999, conversation_key: 'conv_b' })]); + rerender({ conv: convB }); + + await waitFor(() => expect(result.current.messagesLoading).toBe(false)); + expect(olderSignal?.aborted).toBe(true); + expect(mockToastError).not.toHaveBeenCalled(); + }); }); describe('useConversationMessages forward pagination', () => { @@ -454,6 +512,7 @@ describe('useConversationMessages forward pagination', () => { mockGetMessages.mockReset(); mockGetMessagesAround.mockReset(); messageCache.clear(); + mockToastError.mockReset(); }); it('fetchNewerMessages loads newer messages and appends them', async () => { @@ -618,6 +677,69 @@ describe('useConversationMessages forward pagination', () => { expect(result.current.messages[0].text).toBe('latest-msg'); }); + it('aborts stale newer-page requests on conversation switch without toasting', async () => { + const convA: Conversation = { type: 'channel', id: 'ch1', name: 'Channel A' }; + const convB: Conversation = { type: 'channel', id: 'ch2', name: 'Channel B' }; + + mockGetMessagesAround.mockResolvedValueOnce({ + messages: [ + createMessage({ + id: 1, + type: 'CHAN', + conversation_key: 'ch1', + text: 'msg-0', + sender_timestamp: 1700000000, + received_at: 1700000000, + }), + ], + has_older: false, + has_newer: true, + }); + + let newerSignal: AbortSignal | undefined; + mockGetMessages.mockImplementationOnce((_, signal?: AbortSignal) => { + newerSignal = signal; + return new Promise((_, reject) => { + signal?.addEventListener('abort', () => { + reject(new DOMException('The operation was aborted', 'AbortError')); + }); + }); + }); + + const initialProps: { conv: Conversation; target: number | null } = { + conv: convA, + target: 1, + }; + + const { result, rerender } = renderHook( + ({ conv, target }: { conv: Conversation; target: number | null }) => + useConversationMessages(conv, target), + { initialProps } + ); + + await waitFor(() => expect(result.current.messagesLoading).toBe(false)); + + act(() => { + void result.current.fetchNewerMessages(); + }); + + await waitFor(() => expect(result.current.loadingNewer).toBe(true)); + + mockGetMessages.mockResolvedValueOnce([ + createMessage({ + id: 999, + type: 'CHAN', + conversation_key: 'ch2', + text: 'conv-b', + }), + ]); + rerender({ conv: convB, target: null }); + + await waitFor(() => expect(result.current.messagesLoading).toBe(false)); + expect(newerSignal?.aborted).toBe(true); + expect(mockToastError).not.toHaveBeenCalled(); + }); + it('preserves around-loaded messages when the jump target is cleared in the same conversation', async () => { const conv: Conversation = { type: 'channel', id: 'ch1', name: 'Channel' };