Skip to content

Commit db2f518

Browse files
committed
refactor: drop unnecessary channel query when jumping to a reply in channel msg list
1 parent 70f3175 commit db2f518

2 files changed

Lines changed: 124 additions & 19 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
// Shared spies + mutable per-test state. `vi.hoisted` runs before the `vi.mock` factories below, so
5+
// they can close over these.
6+
const mocks = vi.hoisted(() => ({
7+
ingestChannel: vi.fn(),
8+
jumpToMessage: vi.fn(() => Promise.resolve(true)),
9+
open: vi.fn(),
10+
// `query` should never be called by the hook (the jump bootstraps the channel) — asserted below.
11+
query: vi.fn(() => Promise.resolve()),
12+
state: {
13+
channelSlot: undefined as string | undefined,
14+
message: { id: 'reply-1', parent_id: 'parent-1', show_in_channel: true } as
15+
| { id: string; parent_id?: string; show_in_channel?: boolean }
16+
| undefined,
17+
thread: undefined as unknown,
18+
},
19+
}));
20+
21+
vi.mock('../../../../context', () => ({
22+
useChannel: () => ({
23+
cid: 'messaging:general',
24+
initialized: false,
25+
messagePaginator: { jumpToMessage: mocks.jumpToMessage },
26+
query: mocks.query,
27+
}),
28+
useChatContext: () => ({
29+
channelPaginatorsOrchestrator: { ingestChannel: mocks.ingestChannel },
30+
client: {
31+
getThread: vi.fn(),
32+
notifications: { addError: vi.fn() },
33+
threads: { threadsById: {} },
34+
},
35+
}),
36+
useMessageContext: () => ({ message: mocks.state.message }),
37+
useTranslationContext: () => ({ t: (key: string) => key }),
38+
}));
39+
40+
vi.mock('../../../ChatView', () => ({
41+
useChatViewNavigation: () => ({ open: mocks.open }),
42+
useSlotForKey: () => mocks.state.channelSlot,
43+
}));
44+
45+
vi.mock('../../../Threads', () => ({ useThreadContext: () => mocks.state.thread }));
46+
47+
import { useMessageAlsoSentInChannelNavigation } from '../useMessageAlsoSentInChannelNavigation';
48+
49+
const renderNavigation = () =>
50+
renderHook(() => useMessageAlsoSentInChannelNavigation()).result;
51+
52+
describe('useMessageAlsoSentInChannelNavigation', () => {
53+
beforeEach(() => {
54+
mocks.state.channelSlot = undefined;
55+
mocks.state.thread = undefined;
56+
mocks.state.message = { id: 'reply-1', parent_id: 'parent-1', show_in_channel: true };
57+
vi.clearAllMocks();
58+
});
59+
60+
it('derives isInThread / isShownInChannel from context', () => {
61+
mocks.state.thread = { id: 'parent-1' };
62+
const result = renderNavigation();
63+
expect(result.current.isInThread).toBe(true);
64+
expect(result.current.isShownInChannel).toBe(true);
65+
66+
mocks.state.thread = undefined;
67+
mocks.state.message = { id: 'reply-1', show_in_channel: false };
68+
const next = renderNavigation();
69+
expect(next.current.isInThread).toBe(false);
70+
expect(next.current.isShownInChannel).toBe(false);
71+
});
72+
73+
describe('viewReplyInChannel', () => {
74+
it('when the channel is not shown: opens it, jumps, then ingests — with no separate init query', async () => {
75+
mocks.state.channelSlot = undefined; // channel absent from the active view → navigate
76+
77+
const result = renderNavigation();
78+
await result.current.viewReplyInChannel('reply-1');
79+
80+
expect(mocks.open).toHaveBeenCalledTimes(1);
81+
expect(mocks.open).toHaveBeenCalledWith(
82+
expect.objectContaining({ key: 'messaging:general', kind: 'channel' }),
83+
);
84+
expect(mocks.jumpToMessage).toHaveBeenCalledWith('reply-1');
85+
expect(mocks.ingestChannel).toHaveBeenCalledTimes(1);
86+
// The jump's own `channel.query({ messages: { id_around } })` bootstraps the channel, so we
87+
// must NOT fire a separate init query.
88+
expect(mocks.query).not.toHaveBeenCalled();
89+
90+
// Order: open → jumpToMessage → ingestChannel (ingest after the jump so channel.data is loaded).
91+
const openOrder = mocks.open.mock.invocationCallOrder[0];
92+
const jumpOrder = mocks.jumpToMessage.mock.invocationCallOrder[0];
93+
const ingestOrder = mocks.ingestChannel.mock.invocationCallOrder[0];
94+
expect(openOrder).toBeLessThan(jumpOrder);
95+
expect(ingestOrder).toBeGreaterThan(jumpOrder);
96+
});
97+
98+
it('when the channel is already shown: only jumps (no navigate/ingest/query)', async () => {
99+
mocks.state.channelSlot = 'main-channel'; // channel already bound in the active view
100+
101+
const result = renderNavigation();
102+
await result.current.viewReplyInChannel('reply-1');
103+
104+
expect(mocks.jumpToMessage).toHaveBeenCalledWith('reply-1');
105+
expect(mocks.open).not.toHaveBeenCalled();
106+
expect(mocks.ingestChannel).not.toHaveBeenCalled();
107+
expect(mocks.query).not.toHaveBeenCalled();
108+
});
109+
110+
it('defaults the target to the current message id', async () => {
111+
mocks.state.channelSlot = 'main-channel';
112+
const result = renderNavigation();
113+
await result.current.viewReplyInChannel();
114+
expect(mocks.jumpToMessage).toHaveBeenCalledWith('reply-1');
115+
});
116+
});
117+
});

src/components/Message/hooks/useMessageAlsoSentInChannelNavigation.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,29 +58,17 @@ export const useMessageAlsoSentInChannelNavigation =
5858

5959
const viewReplyInChannel = async (messageId = message?.id) => {
6060
if (!messageId) return;
61-
if (!channelSlot) {
62-
// The channel isn't shown in the active view — navigate to it. `open` resolves the
63-
// channel kind's view from the slot registry and switches there (no hard-coded view name);
64-
// `ingestChannel` surfaces it in the channel list(s) incrementally instead of forcing a
65-
// full list re-query.
61+
// The channel isn't shown in the active view when it has no slot — we need to navigate to it.
62+
const needsNavigation = !channelSlot;
63+
if (needsNavigation) {
6664
open({ key: channel.cid ?? undefined, kind: 'channel', source: channel });
67-
// Load the channel's state (members, config, read state; registers it in
68-
// `client.activeChannels`) so `ingestChannel` below can match it against paginator filters
69-
// and the panel has data to render. `messages: { limit: 0 }` fetches no messages — the
70-
// `jumpToMessage` call at the end loads the message window around the target, so pulling a
71-
// default page here would be wasted, immediately-superseded work. `channel.initialized` is
72-
// only set by `channel.watch()` (the channel list watches its channels), so this guard
73-
// effectively means "not already loaded via the list"; a plain `query()` neither watches
74-
// nor flips `initialized`.
75-
if (!channel.initialized) {
76-
await channel.query({ messages: { limit: 0 } });
77-
}
78-
// Surface the (now initialized) channel in the list(s) so its data is available for filter
79-
// matching; `ingestChannel` dedupes by cid and inserts in sort order — safe to call always.
80-
channelPaginatorsOrchestrator.ingestChannel(channel);
8165
}
8266

8367
await channel.messagePaginator.jumpToMessage(messageId);
68+
69+
if (needsNavigation) {
70+
channelPaginatorsOrchestrator.ingestChannel(channel);
71+
}
8472
};
8573

8674
const viewReplyInThread = async (

0 commit comments

Comments
 (0)