Skip to content

Commit 8a6e018

Browse files
authored
Merge pull request #507 from GetStream/pagination-inconsistencies
Pagination inconsistencies
2 parents 852bac5 + cf73e5d commit 8a6e018

File tree

3 files changed

+134
-20
lines changed

3 files changed

+134
-20
lines changed

projects/stream-chat-angular/src/lib/channel.service.spec.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('ChannelService', () => {
114114
});
115115

116116
it('should use provided options params', async () => {
117-
const options: ChannelOptions = { offset: 5 };
117+
const options: ChannelOptions = { limit: 5 };
118118
await init(undefined, undefined, options);
119119

120120
expect(mockChatClient.queryChannels).toHaveBeenCalledWith(
@@ -245,6 +245,36 @@ describe('ChannelService', () => {
245245
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
246246
});
247247

248+
it('should set pagination options correctly if #customPaginator is provided', async () => {
249+
service.customPaginator = (
250+
channelQueryResult: Channel<DefaultStreamChatGenerics>[]
251+
) => {
252+
const lastChannel = channelQueryResult[channelQueryResult.length - 1];
253+
if (!lastChannel) {
254+
return {
255+
type: 'filter',
256+
paginationFilter: {},
257+
};
258+
} else {
259+
return {
260+
type: 'filter',
261+
paginationFilter: {
262+
cid: { $gte: lastChannel.cid },
263+
},
264+
};
265+
}
266+
};
267+
268+
await init();
269+
270+
expect(service['nextPageConfiguration']).toEqual({
271+
type: 'filter',
272+
paginationFilter: {
273+
cid: { $gte: jasmine.any(String) },
274+
},
275+
});
276+
});
277+
248278
it('should not set active channel if #shouldSetActiveChannel is false', async () => {
249279
const activeChannelSpy = jasmine.createSpy();
250280
service.activeChannel$.subscribe(activeChannelSpy);
@@ -371,16 +401,30 @@ describe('ChannelService', () => {
371401
expect(spy).toHaveBeenCalledWith(false);
372402
});
373403

374-
it('should load more channels', async () => {
404+
it('should load more channels and filter duplicates', async () => {
375405
await init();
406+
407+
// Check that offset is set properly after query
408+
expect(service['nextPageConfiguration']).toEqual({
409+
type: 'offset',
410+
offset: service.channels.length,
411+
});
412+
376413
mockChatClient.queryChannels.calls.reset();
414+
const existingChannel = service.channels[0];
415+
const newChannel = generateMockChannels(1)[0];
416+
newChannel.cid = 'this-channel-is-not-yet-loaded';
417+
mockChatClient.queryChannels.and.resolveTo([existingChannel, newChannel]);
418+
const prevChannelCount = service.channels.length;
377419
await service.loadMoreChannels();
378420

379421
expect(mockChatClient.queryChannels).toHaveBeenCalledWith(
380422
jasmine.any(Object),
381423
jasmine.any(Object),
382424
jasmine.any(Object)
383425
);
426+
427+
expect(service.channels.length).toEqual(prevChannelCount + 1);
384428
});
385429

386430
it('should set active channel', async () => {
@@ -1942,14 +1986,14 @@ describe('ChannelService', () => {
19421986
});
19431987

19441988
it('should reset pagination options after reconnect', async () => {
1945-
await init(undefined, undefined, { offset: 20 });
1989+
await init(undefined, undefined, { limit: 20 });
19461990
mockChatClient.queryChannels.calls.reset();
19471991
events$.next({ eventType: 'connection.recovered' } as ClientEvent);
19481992

19491993
expect(mockChatClient.queryChannels).toHaveBeenCalledWith(
19501994
jasmine.any(Object),
19511995
jasmine.any(Object),
1952-
jasmine.objectContaining({ offset: 0 })
1996+
{ limit: 20, state: true, presence: true, watch: true, message_limit: 25 }
19531997
);
19541998
});
19551999

projects/stream-chat-angular/src/lib/channel.service.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
DefaultStreamChatGenerics,
3535
MessageInput,
3636
MessageReactionType,
37+
NextPageConfiguration,
3738
StreamMessage,
3839
} from './types';
3940

@@ -293,6 +294,10 @@ export class ChannelService<
293294
beforeUpdateMessage?: (
294295
message: StreamMessage<T>
295296
) => StreamMessage<T> | Promise<StreamMessage<T>>;
297+
/**
298+
* By default the SDK uses an offset based pagination, you can change/extend this by providing your own custom paginator method. It will be called with the result of the latest channel query.
299+
*/
300+
customPaginator?: (channelQueryResult: Channel<T>[]) => NextPageConfiguration;
296301
private channelsSubject = new BehaviorSubject<Channel<T>[] | undefined>(
297302
undefined
298303
);
@@ -374,6 +379,7 @@ export class ChannelService<
374379
this.activeParentMessageIdSubject.next(message?.id);
375380
};
376381
private dismissErrorNotification?: Function;
382+
private nextPageConfiguration?: NextPageConfiguration;
377383

378384
constructor(
379385
private chatClientService: ChatClientService<T>,
@@ -650,15 +656,14 @@ export class ChannelService<
650656
) {
651657
this.filters = filters;
652658
this.options = {
653-
offset: 0,
654659
limit: 25,
655660
state: true,
656661
presence: true,
657662
watch: true,
658663
message_limit: this.messagePageSize,
659664
...options,
660665
};
661-
this.sort = sort || { last_message_at: -1, updated_at: -1 };
666+
this.sort = sort || { last_message_at: -1 };
662667
this.shouldSetActiveChannel = shouldSetActiveChannel;
663668
this.clientEventsSubscription = this.chatClientService.events$.subscribe(
664669
(notification) => void this.handleNotification(notification)
@@ -696,7 +701,6 @@ export class ChannelService<
696701
* Loads the next page of channels. The page size can be set in the [query option](https://getstream.io/chat/docs/javascript/query_channels/?language=javascript#query-options) object.
697702
*/
698703
async loadMoreChannels() {
699-
this.options!.offset = this.channels.length!;
700704
await this.queryChannels(false);
701705
}
702706

@@ -1108,9 +1112,7 @@ export class ChannelService<
11081112
}
11091113
this.isStateRecoveryInProgress = true;
11101114
try {
1111-
if (this.options) {
1112-
this.options.offset = 0;
1113-
}
1115+
this.nextPageConfiguration = undefined;
11141116
// If channel list is not inited, we set the active channel
11151117
const shoulSetActiveChannel =
11161118
this.shouldSetActiveChannel &&
@@ -1378,6 +1380,20 @@ export class ChannelService<
13781380
await activeChannel?.stopTyping(parentId);
13791381
}
13801382

1383+
/**
1384+
* The current list of channels
1385+
*/
1386+
get channels() {
1387+
return this.channelsSubject.getValue() || [];
1388+
}
1389+
1390+
/**
1391+
* The current active channel
1392+
*/
1393+
get activeChannel() {
1394+
return this.activeChannelSubject.getValue() || undefined;
1395+
}
1396+
13811397
private messageUpdated(event: Event<T>) {
13821398
this.ngZone.run(() => {
13831399
const isThreadReply = event.message && event.message.parent_id;
@@ -1473,28 +1489,55 @@ export class ChannelService<
14731489
) {
14741490
try {
14751491
this.channelQueryStateSubject.next({ state: 'in-progress' });
1492+
let filters: ChannelFilters<T>;
1493+
let options: ChannelOptions;
1494+
if (this.nextPageConfiguration) {
1495+
if (this.nextPageConfiguration.type === 'filter') {
1496+
filters = {
1497+
...this.filters!,
1498+
...this.nextPageConfiguration.paginationFilter,
1499+
};
1500+
options = this.options as ChannelOptions;
1501+
} else {
1502+
options = {
1503+
...this.options,
1504+
offset: this.nextPageConfiguration.offset,
1505+
};
1506+
filters = this.filters!;
1507+
}
1508+
} else {
1509+
filters = this.filters!;
1510+
options = this.options as ChannelOptions;
1511+
}
14761512
const channels = await this.chatClientService.chatClient.queryChannels(
1477-
this.filters!,
1513+
filters,
14781514
this.sort || {},
1479-
this.options
1515+
options
14801516
);
1517+
this.setNextPageConfiguration(channels);
14811518
channels.forEach((c) => this.watchForChannelEvents(c));
14821519
const prevChannels = recoverState
14831520
? []
14841521
: this.channelsSubject.getValue() || [];
1485-
this.channelsSubject.next([...prevChannels, ...channels]);
1522+
const filteredChannels = channels.filter(
1523+
(channel) =>
1524+
!prevChannels.find(
1525+
(existingChannel) => existingChannel.cid === channel.cid
1526+
)
1527+
);
1528+
this.channelsSubject.next([...prevChannels, ...filteredChannels]);
14861529
let currentActiveChannel = this.activeChannelSubject.getValue();
14871530
if (
1488-
channels.length > 0 &&
1531+
filteredChannels.length > 0 &&
14891532
!currentActiveChannel &&
14901533
shouldSetActiveChannel
14911534
) {
1492-
this.setAsActiveChannel(channels[0]);
1535+
this.setAsActiveChannel(filteredChannels[0]);
14931536
currentActiveChannel = this.activeChannelSubject.getValue();
14941537
}
14951538
if (
14961539
recoverState &&
1497-
!channels.find((c) => c.cid === currentActiveChannel?.cid)
1540+
!filteredChannels.find((c) => c.cid === currentActiveChannel?.cid)
14981541
) {
14991542
this.deselectActiveChannel();
15001543
}
@@ -1711,10 +1754,6 @@ export class ChannelService<
17111754
}
17121755
}
17131756

1714-
private get channels() {
1715-
return this.channelsSubject.getValue() || [];
1716-
}
1717-
17181757
private get canSendReadEvents() {
17191758
const channel = this.activeChannelSubject.getValue();
17201759
if (!channel) {
@@ -1888,4 +1927,18 @@ export class ChannelService<
18881927
void channel.markRead();
18891928
}
18901929
}
1930+
1931+
private setNextPageConfiguration(channelQueryResult: Channel<T>[]) {
1932+
if (this.customPaginator) {
1933+
this.nextPageConfiguration = this.customPaginator(channelQueryResult);
1934+
} else {
1935+
this.nextPageConfiguration = {
1936+
type: 'offset',
1937+
offset:
1938+
(this.nextPageConfiguration?.type === 'offset'
1939+
? this.nextPageConfiguration.offset
1940+
: 0) + channelQueryResult.length,
1941+
};
1942+
}
1943+
}
18911944
}

projects/stream-chat-angular/src/lib/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Observable, Subject } from 'rxjs';
33
import type {
44
Attachment,
55
Channel,
6+
ChannelFilters,
67
ChannelMemberResponse,
78
CommandResponse,
89
Event,
@@ -360,3 +361,19 @@ export type MessageInput<
360361
quotedMessageId: string | undefined;
361362
customData: undefined | Partial<T['messageType']>;
362363
};
364+
365+
export type OffsetNextPageConfiguration = {
366+
type: 'offset';
367+
offset: number;
368+
};
369+
370+
export type FiltertNextPageConfiguration<
371+
T extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
372+
> = {
373+
type: 'filter';
374+
paginationFilter: ChannelFilters<T>;
375+
};
376+
377+
export type NextPageConfiguration =
378+
| OffsetNextPageConfiguration
379+
| FiltertNextPageConfiguration;

0 commit comments

Comments
 (0)