Skip to content

Commit 815ff6c

Browse files
fix: load 50 messages with hideSystemMessages as true on old workspaces (#7305)
* fix: fetch enough history when system messages are hidden * trying to load messages * load code improvements * feat: improve test useMessages * fix: dedupe concurrent room history fetches on cold open * refactor: simplify loadMessagesForRoom and document history loader terms * fix: tests
1 parent 3961d4d commit 815ff6c

8 files changed

Lines changed: 558 additions & 49 deletions

File tree

app/actions/actionsTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export const ROOM = createRequestTypes('ROOM', [
2222
'FORWARD',
2323
'USER_TYPING',
2424
'HISTORY_REQUEST',
25-
'HISTORY_FINISHED'
25+
'HISTORY_FINISHED',
26+
'HISTORY_UI_LOADER_PUSH',
27+
'HISTORY_UI_LOADER_POP'
2628
]);
2729
export const INQUIRY = createRequestTypes('INQUIRY', [
2830
...defaultTypes,

app/actions/room.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,24 @@ export interface IRoomHistoryFinished extends Action {
5959
loaderId: string;
6060
}
6161

62+
export interface IRoomHistoryUiLoaderPush extends Action {
63+
loaderId: string;
64+
}
65+
66+
export interface IRoomHistoryUiLoaderPop extends Action {
67+
loaderId: string;
68+
}
69+
6270
export type TActionsRoom = TSubscribeRoom &
6371
TUnsubscribeRoom &
6472
ILeaveRoom &
6573
IDeleteRoom &
6674
IForwardRoom &
6775
IUserTyping &
6876
IRoomHistoryRequest &
69-
IRoomHistoryFinished;
77+
IRoomHistoryFinished &
78+
IRoomHistoryUiLoaderPush &
79+
IRoomHistoryUiLoaderPop;
7080

7181
export function subscribeRoom(rid: string): TSubscribeRoom {
7282
return {
@@ -138,3 +148,17 @@ export function roomHistoryFinished({ loaderId }: { loaderId: string }): IRoomHi
138148
loaderId
139149
};
140150
}
151+
152+
export function roomHistoryUiLoaderPush({ loaderId }: { loaderId: string }): IRoomHistoryUiLoaderPush {
153+
return {
154+
type: ROOM.HISTORY_UI_LOADER_PUSH,
155+
loaderId
156+
};
157+
}
158+
159+
export function roomHistoryUiLoaderPop({ loaderId }: { loaderId: string }): IRoomHistoryUiLoaderPop {
160+
return {
161+
type: ROOM.HISTORY_UI_LOADER_POP,
162+
loaderId
163+
};
164+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { loadMessagesForRoom } from './loadMessagesForRoom';
2+
import sdk from '../services/sdk';
3+
import { ROOM } from '../../actions/actionsTypes';
4+
import { getMessageById } from '../database/services/Message';
5+
import { getSubscriptionByRoomId } from '../database/services/Subscription';
6+
import updateMessages from './updateMessages';
7+
import { store } from '../store/auxStore';
8+
9+
jest.mock('../services/sdk', () => ({
10+
__esModule: true,
11+
default: {
12+
get: jest.fn()
13+
}
14+
}));
15+
16+
jest.mock('../database/services/Message', () => ({
17+
getMessageById: jest.fn()
18+
}));
19+
20+
jest.mock('../database/services/Subscription', () => ({
21+
getSubscriptionByRoomId: jest.fn(() => Promise.resolve(null))
22+
}));
23+
24+
jest.mock('../store/auxStore', () => ({
25+
store: {
26+
getState: jest.fn(() => ({
27+
settings: { Hide_System_Messages: ['uj'] }
28+
})),
29+
dispatch: jest.fn()
30+
}
31+
}));
32+
33+
jest.mock('./updateMessages', () => jest.fn());
34+
35+
const mockedSdkGet = sdk.get as jest.MockedFunction<typeof sdk.get>;
36+
const mockedGetMessageById = getMessageById as jest.MockedFunction<typeof getMessageById>;
37+
const mockedUpdateMessages = updateMessages as jest.MockedFunction<typeof updateMessages>;
38+
const mockedDispatch = store.dispatch as jest.MockedFunction<typeof store.dispatch>;
39+
const mockedGetSubscriptionByRoomId = getSubscriptionByRoomId as jest.MockedFunction<typeof getSubscriptionByRoomId>;
40+
41+
const buildMessage = ({ id, ts, t }: { id: string; ts: string; t?: string }) =>
42+
({
43+
_id: id,
44+
rid: 'ROOM_ID',
45+
ts,
46+
...(t ? { t } : {})
47+
} as any);
48+
49+
describe('loadMessagesForRoom', () => {
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
mockedGetMessageById.mockResolvedValue(null);
53+
mockedUpdateMessages.mockResolvedValue(0);
54+
mockedGetSubscriptionByRoomId.mockResolvedValue(null as any);
55+
});
56+
57+
const buildHiddenBatch = (prefix: string, baseSeconds: number) =>
58+
Array.from({ length: 50 }, (_, index) =>
59+
buildMessage({
60+
id: `${prefix}-${index + 1}`,
61+
ts: new Date(Date.UTC(2024, 0, 1, 0, 0, baseSeconds - index)).toISOString(),
62+
t: 'uj'
63+
})
64+
);
65+
66+
it('fetches additional history batches until it fills the visible page when hidden system messages consume the first batch', async () => {
67+
const firstBatch = Array.from({ length: 50 }, (_, index) =>
68+
buildMessage({
69+
id: `first-${index + 1}`,
70+
ts: new Date(Date.UTC(2024, 0, 1, 0, 0, 50 - index)).toISOString(),
71+
t: index < 49 ? 'uj' : undefined
72+
})
73+
);
74+
const secondBatch = Array.from({ length: 50 }, (_, index) =>
75+
buildMessage({
76+
id: `second-${index + 1}`,
77+
ts: new Date(Date.UTC(2023, 11, 31, 23, 59, 50 - index)).toISOString(),
78+
t: index === 49 ? 'uj' : undefined
79+
})
80+
);
81+
82+
mockedSdkGet
83+
.mockResolvedValueOnce({ success: true, messages: firstBatch } as any)
84+
.mockResolvedValueOnce({ success: true, messages: secondBatch } as any);
85+
86+
await loadMessagesForRoom({
87+
rid: 'ROOM_ID',
88+
t: 'c'
89+
});
90+
91+
expect(mockedSdkGet).toHaveBeenCalledTimes(2);
92+
expect(mockedSdkGet).toHaveBeenNthCalledWith(
93+
2,
94+
'channels.history',
95+
expect.objectContaining({
96+
roomId: 'ROOM_ID',
97+
latest: firstBatch[firstBatch.length - 1].ts
98+
})
99+
);
100+
101+
expect(mockedUpdateMessages).toHaveBeenCalledTimes(2);
102+
expect(mockedUpdateMessages).toHaveBeenNthCalledWith(
103+
1,
104+
expect.objectContaining({
105+
rid: 'ROOM_ID',
106+
update: expect.arrayContaining([
107+
expect.objectContaining({ _id: 'first-50' }),
108+
expect.objectContaining({ _id: 'load-more-first-50', t: 'load_more' })
109+
])
110+
})
111+
);
112+
expect(mockedUpdateMessages).toHaveBeenNthCalledWith(
113+
2,
114+
expect.objectContaining({
115+
rid: 'ROOM_ID',
116+
update: expect.arrayContaining([
117+
expect.objectContaining({ _id: 'first-50' }),
118+
expect.objectContaining({ _id: 'second-49' }),
119+
expect.objectContaining({ _id: 'load-more-second-50', t: 'load_more' })
120+
])
121+
})
122+
);
123+
124+
expect(mockedDispatch).toHaveBeenCalledWith(
125+
expect.objectContaining({
126+
type: ROOM.HISTORY_UI_LOADER_PUSH,
127+
loaderId: 'load-more-first-50'
128+
})
129+
);
130+
expect(mockedDispatch).toHaveBeenCalledWith(
131+
expect.objectContaining({
132+
type: ROOM.HISTORY_UI_LOADER_POP,
133+
loaderId: 'load-more-first-50'
134+
})
135+
);
136+
});
137+
138+
it('stops fetching after MAX_BATCHES even when the visible page is still unfilled', async () => {
139+
// Every batch is fully hidden, so visibleMainMessagesCount never reaches COUNT
140+
mockedSdkGet.mockResolvedValue({ success: true, messages: buildHiddenBatch('batch', 50) } as any);
141+
142+
await loadMessagesForRoom({ rid: 'ROOM_ID', t: 'c' });
143+
144+
// MAX_BATCHES = 10
145+
expect(mockedSdkGet).toHaveBeenCalledTimes(10);
146+
});
147+
148+
it('does not append a trailing load-more when the last batch was not full', async () => {
149+
const partialBatch = Array.from({ length: 30 }, (_, index) =>
150+
buildMessage({
151+
id: `partial-${index + 1}`,
152+
ts: new Date(Date.UTC(2024, 0, 1, 0, 0, 30 - index)).toISOString()
153+
})
154+
);
155+
156+
mockedSdkGet.mockResolvedValueOnce({ success: true, messages: partialBatch } as any);
157+
158+
await loadMessagesForRoom({ rid: 'ROOM_ID', t: 'c' });
159+
160+
expect(mockedSdkGet).toHaveBeenCalledTimes(1);
161+
expect(mockedUpdateMessages).toHaveBeenCalledTimes(1);
162+
const finalUpdate = mockedUpdateMessages.mock.calls[0][0] as { update: { _id: string; t?: string }[] };
163+
expect(finalUpdate.update).toHaveLength(30);
164+
expect(finalUpdate.update.find(m => m.t === 'load_more')).toBeUndefined();
165+
expect(mockedDispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: ROOM.HISTORY_UI_LOADER_PUSH }));
166+
});
167+
168+
it('pops the ui loader when a recursive batch fetch fails after the loader was pushed', async () => {
169+
const firstBatch = buildHiddenBatch('first', 50);
170+
const networkError = new Error('boom');
171+
172+
mockedSdkGet.mockResolvedValueOnce({ success: true, messages: firstBatch } as any).mockRejectedValueOnce(networkError);
173+
174+
await expect(loadMessagesForRoom({ rid: 'ROOM_ID', t: 'c' })).rejects.toBe(networkError);
175+
176+
expect(mockedDispatch).toHaveBeenCalledWith(
177+
expect.objectContaining({
178+
type: ROOM.HISTORY_UI_LOADER_PUSH,
179+
loaderId: 'load-more-first-50'
180+
})
181+
);
182+
expect(mockedDispatch).toHaveBeenCalledWith(
183+
expect.objectContaining({
184+
type: ROOM.HISTORY_UI_LOADER_POP,
185+
loaderId: 'load-more-first-50'
186+
})
187+
);
188+
});
189+
190+
it('falls back to settings.Hide_System_Messages when sub.sysMes is a boolean (not an array)', async () => {
191+
mockedGetSubscriptionByRoomId.mockResolvedValue({ sysMes: true } as any);
192+
193+
const firstBatch = buildHiddenBatch('first', 50);
194+
const secondBatch = Array.from({ length: 50 }, (_, index) =>
195+
buildMessage({
196+
id: `second-${index + 1}`,
197+
ts: new Date(Date.UTC(2023, 11, 31, 23, 59, 50 - index)).toISOString()
198+
})
199+
);
200+
201+
mockedSdkGet
202+
.mockResolvedValueOnce({ success: true, messages: firstBatch } as any)
203+
.mockResolvedValueOnce({ success: true, messages: secondBatch } as any);
204+
205+
await loadMessagesForRoom({ rid: 'ROOM_ID', t: 'c' });
206+
207+
// settings.Hide_System_Messages = ['uj'] (from top-level mock) → first batch hidden → must recurse
208+
expect(mockedSdkGet).toHaveBeenCalledTimes(2);
209+
});
210+
211+
it('does not insert an intermediate load-more when a loaderItem is provided', async () => {
212+
const firstBatch = buildHiddenBatch('first', 50);
213+
const secondBatch = Array.from({ length: 50 }, (_, index) =>
214+
buildMessage({
215+
id: `second-${index + 1}`,
216+
ts: new Date(Date.UTC(2023, 11, 31, 23, 59, 50 - index)).toISOString()
217+
})
218+
);
219+
220+
mockedSdkGet
221+
.mockResolvedValueOnce({ success: true, messages: firstBatch } as any)
222+
.mockResolvedValueOnce({ success: true, messages: secondBatch } as any);
223+
224+
await loadMessagesForRoom({
225+
rid: 'ROOM_ID',
226+
t: 'c',
227+
loaderItem: { id: 'tapped-load-more' } as any
228+
});
229+
230+
// Only the outer write — the intermediate batch-1 loader insert is skipped
231+
expect(mockedUpdateMessages).toHaveBeenCalledTimes(1);
232+
expect(mockedDispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: ROOM.HISTORY_UI_LOADER_PUSH }));
233+
expect(mockedDispatch).not.toHaveBeenCalledWith(expect.objectContaining({ type: ROOM.HISTORY_UI_LOADER_POP }));
234+
});
235+
});

0 commit comments

Comments
 (0)