Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add channel messages pagination indicators #1332

Merged
merged 12 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/channel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChannelState } from './channel_state';
import { logChatPromiseExecution, normalizeQuerySort } from './utils';
import { logChatPromiseExecution, messageSetPagination, normalizeQuerySort } from './utils';
import { StreamChat } from './client';
import {
APIResponse,
Expand Down Expand Up @@ -58,6 +58,7 @@ import {
AscDesc,
} from './types';
import { Role } from './permissions';
import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants';

/**
* Channel - The Channel class manages it's own state.
Expand Down Expand Up @@ -1002,7 +1003,7 @@ export class Channel<StreamChatGenerics extends ExtendableGenerics = DefaultGene
* @return {Promise<QueryChannelAPIResponse<StreamChatGenerics>>} Returns a query response
*/
async query(
options: ChannelQueryOptions<StreamChatGenerics>,
options?: ChannelQueryOptions<StreamChatGenerics>,
messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
) {
// Make sure we wait for the connect promise if there is a pending one
Expand Down Expand Up @@ -1046,6 +1047,16 @@ export class Channel<StreamChatGenerics extends ExtendableGenerics = DefaultGene

// add any messages to our channel state
const { messageSet } = this._initializeState(state, messageSetToAddToIfDoesNotExist);
messageSet.pagination = {
...messageSet.pagination,
...messageSetPagination({
parentSet: messageSet,
messagePaginationOptions: options?.messages,
requestedPageSize: options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE,
returnedPage: state.messages,
logger: this.getClient().logger,
}),
};

const areCapabilitiesChanged =
[...(state.channel.own_capabilities || [])].sort().join() !==
Expand Down
30 changes: 23 additions & 7 deletions src/channel_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExtendableGenerics,
FormatMessageResponse,
MessageResponse,
MessageSet,
MessageSetType,
PendingMessageResponse,
PollResponse,
Expand All @@ -14,6 +15,7 @@ import {
UserResponse,
} from './types';
import { addToMessageList } from './utils';
import { DEFAULT_MESSAGE_SET_PAGINATION } from './constants';

type ChannelReadStatus<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = Record<
string,
Expand Down Expand Up @@ -56,11 +58,7 @@ export class ChannelState<StreamChatGenerics extends ExtendableGenerics = Defaul
* The state manages these lists and merges them when lists overlap
* The messages array contains the currently active set
*/
messageSets: {
isCurrent: boolean;
isLatest: boolean;
messages: Array<ReturnType<ChannelState<StreamChatGenerics>['formatMessage']>>;
}[] = [];
messageSets: MessageSet[] = [];
constructor(channel: Channel<StreamChatGenerics>) {
this._channel = channel;
this.watcher_count = 0;
Expand Down Expand Up @@ -108,6 +106,10 @@ export class ChannelState<StreamChatGenerics extends ExtendableGenerics = Defaul
this.messageSets[index].messages = messages;
}

get messagePagination() {
return this.messageSets.find((s) => s.isCurrent)?.pagination || DEFAULT_MESSAGE_SET_PAGINATION;
}

/**
* addMessageSorted - Add a message to the state
*
Expand Down Expand Up @@ -717,14 +719,15 @@ export class ChannelState<StreamChatGenerics extends ExtendableGenerics = Defaul
}

initMessages() {
this.messageSets = [{ messages: [], isLatest: true, isCurrent: true }];
this.messageSets = [{ messages: [], isLatest: true, isCurrent: true, pagination: DEFAULT_MESSAGE_SET_PAGINATION }];
}

/**
* loadMessageIntoState - Loads a given message (and messages around it) into the state
*
* @param {string} messageId The id of the message, or 'latest' to indicate switching to the latest messages
* @param {string} parentMessageId The id of the parent message, if we want load a thread reply
* @param {number} limit The page size if the message has to be queried from the server
*/
async loadMessageIntoState(messageId: string | 'latest', parentMessageId?: string, limit = 25) {
let messageSetIndex: number;
Expand Down Expand Up @@ -820,7 +823,12 @@ export class ChannelState<StreamChatGenerics extends ExtendableGenerics = Defaul
targetMessageSetIndex = overlappingMessageSetIndices[0];
// No new message set is created if newMessages only contains thread replies
} else if (newMessages.some((m) => !m.parent_id)) {
this.messageSets.push({ messages: [], isCurrent: false, isLatest: false });
this.messageSets.push({
messages: [],
isCurrent: false,
isLatest: false,
pagination: DEFAULT_MESSAGE_SET_PAGINATION,
});
targetMessageSetIndex = this.messageSets.length - 1;
}
break;
Expand All @@ -846,6 +854,14 @@ export class ChannelState<StreamChatGenerics extends ExtendableGenerics = Defaul
sources.forEach((messageSet) => {
target.isLatest = target.isLatest || messageSet.isLatest;
target.isCurrent = target.isCurrent || messageSet.isCurrent;
target.pagination.hasPrev =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging pagination objects when merging message sets.

messageSet.messages[0].created_at < target.messages[0].created_at
? messageSet.pagination.hasPrev
: target.pagination.hasPrev;
target.pagination.hasNext =
target.messages.slice(-1)[0].created_at < messageSet.messages.slice(-1)[0].created_at
? messageSet.pagination.hasNext
: target.pagination.hasNext;
messagesToAdd = [...messagesToAdd, ...messageSet.messages];
});
sources.forEach((s) => this.messageSets.splice(this.messageSets.indexOf(s), 1));
Expand Down
30 changes: 22 additions & 8 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isFunction,
isOnline,
isOwnUserBaseProperty,
messageSetPagination,
normalizeQuerySort,
randomId,
retryInterval,
Expand Down Expand Up @@ -207,6 +208,7 @@ import {
import { InsightMetrics, postInsights } from './insights';
import { Thread } from './thread';
import { Moderation } from './moderation';
import { DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE } from './constants';

function isString(x: unknown): x is string {
return typeof x === 'string' || x instanceof String;
Expand Down Expand Up @@ -1601,7 +1603,7 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
},
});

return this.hydrateActiveChannels(data.channels, stateOptions);
return this.hydrateActiveChannels(data.channels, stateOptions, options);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spaghetti code architecture forced me to do this.

}

/**
Expand Down Expand Up @@ -1638,26 +1640,38 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
hydrateActiveChannels(
channelsFromApi: ChannelAPIResponse<StreamChatGenerics>[] = [],
stateOptions: ChannelStateOptions = {},
queryChannelsOptions?: ChannelOptions,
) {
const { skipInitialization, offlineMode = false } = stateOptions;

for (const channelState of channelsFromApi) {
this._addChannelConfig(channelState.channel);
}

const channels: Channel<StreamChatGenerics>[] = [];

for (const channelState of channelsFromApi) {
this._addChannelConfig(channelState.channel);
const c = this.channel(channelState.channel.type, channelState.channel.id);
c.data = channelState.channel;
c.offlineMode = offlineMode;
c.initialized = !offlineMode;

let updatedMessagesSet;
if (skipInitialization === undefined) {
c._initializeState(channelState, 'latest');
const { messageSet } = c._initializeState(channelState, 'latest');
updatedMessagesSet = messageSet;
} else if (!skipInitialization.includes(channelState.channel.id)) {
c.state.clearMessages();
c._initializeState(channelState, 'latest');
const { messageSet } = c._initializeState(channelState, 'latest');
updatedMessagesSet = messageSet;
}

if (updatedMessagesSet) {
updatedMessagesSet.pagination = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pagination indicators are calculated once the requested page is merged into the parent message set.

...updatedMessagesSet.pagination,
...messageSetPagination({
parentSet: updatedMessagesSet,
requestedPageSize: queryChannelsOptions?.message_limit || DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE,
returnedPage: channelState.messages,
logger: this.logger,
}),
};
}

channels.push(c);
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE = 25;
export const DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100;

export const DEFAULT_MESSAGE_SET_PAGINATION = { hasNext: true, hasPrev: true };
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ export type PaginationOptions = {
id_lt?: string;
id_lte?: string;
limit?: number;
offset?: number;
offset?: number; // should be avoided with channel.query()
};

export type MessagePaginationOptions = PaginationOptions & {
Expand Down Expand Up @@ -2900,6 +2900,12 @@ export type ImportTask = {
};

export type MessageSetType = 'latest' | 'current' | 'new';
export type MessageSet<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
isCurrent: boolean;
isLatest: boolean;
messages: FormatMessageResponse<StreamChatGenerics>[];
pagination: { hasNext: boolean; hasPrev: boolean };
};

export type PushProviderUpsertResponse = {
push_provider: PushProvider;
Expand Down
Loading
Loading