Skip to content
Open
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
4 changes: 1 addition & 3 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
normalizeQuerySort,
} from './utils';
import type { StreamChat } from './client';
import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants';
import type {
AIState,
APIResponse,
Expand Down Expand Up @@ -1562,8 +1561,7 @@ export class Channel {
...messageSetPagination({
parentSet: messageSet,
messagePaginationOptions: options?.messages,
requestedPageSize:
options?.messages?.limit ?? DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE,
requestedPageSize: options?.messages?.limit,
returnedPage: state.messages,
logger: this.getClient().logger,
}),
Expand Down
5 changes: 1 addition & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ import { InsightMetrics, postInsights } from './insights';
import { Thread } from './thread';
import { Moderation } from './moderation';
import { ThreadManager } from './thread_manager';
import { DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE } from './constants';
import { PollManager } from './poll_manager';
import type {
ChannelManagerEventHandlerOverrides,
Expand Down Expand Up @@ -2041,9 +2040,7 @@ export class StreamChat {
...updatedMessagesSet.pagination,
...messageSetPagination({
parentSet: updatedMessagesSet,
requestedPageSize:
queryChannelsOptions?.message_limit ||
DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE,
requestedPageSize: queryChannelsOptions?.message_limit,
returnedPage: channelState.messages,
logger: this.logger,
}),
Expand Down
3 changes: 0 additions & 3 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
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 = Object.freeze({
hasNext: false,
hasPrev: false,
});
export const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 100 MB
export const API_MAX_FILES_ALLOWED_PER_MESSAGE = 10;
export const MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY = 100;
export const RESERVED_UPDATED_MESSAGE_FIELDS = Object.freeze({
// Dates should not be converted back to ISO strings as JS looses precision on them (milliseconds)
created_at: true,
Expand Down
9 changes: 4 additions & 5 deletions src/messageComposer/middleware/textComposer/mentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import type {
UserSort,
} from '../../../types';
import type { Channel } from '../../../channel';
import { MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY } from '../../../constants';
import type { Middleware } from '../../../middleware';
import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';

Expand Down Expand Up @@ -107,8 +106,8 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
}

get allMembersLoadedWithInitialChannelQuery() {
const countLoadedMembers = Object.keys(this.channel.state.members || {}).length;
return countLoadedMembers < MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY;
const { state, data } = this.channel;
return Object.keys(state.members).length === (data?.member_count ?? 0);
}

toUserSuggestion = (user: UserResponse): UserSuggestion => ({
Expand Down Expand Up @@ -236,13 +235,13 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
};
};

queryUsers = async (searchQuery: string) => {
queryUsers = async (searchQuery: string): Promise<UserResponse[]> => {
const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery);
const { users } = await this.client.queryUsers(filters, sort, options);
return users;
};

queryMembers = async (searchQuery: string) => {
queryMembers = async (searchQuery: string): Promise<UserResponse[]> => {
const { filters, sort, options } = this.prepareQueryMembersParams(searchQuery);
const response = await this.channel.queryMembers(filters, sort, options);

Expand Down
35 changes: 27 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,12 +809,30 @@ export const uniqBy = <T>(

type MessagePaginationUpdatedParams = {
parentSet: MessageSet;
requestedPageSize: number;
requestedPageSize?: number;
returnedPage: MessageResponse[];
logger?: Logger;
messagePaginationOptions?: MessagePaginationOptions;
};

type PaginationBoundaries = Pick<
MessagePaginationUpdatedParams,
'requestedPageSize' | 'returnedPage'
>;

const toPageSize = ({ requestedPageSize, returnedPage }: PaginationBoundaries) =>
typeof requestedPageSize === 'number'
? requestedPageSize
: Math.max(returnedPage.length, 1);

const hasMoreByPageSize = ({ requestedPageSize, returnedPage }: PaginationBoundaries) => {
// If the caller doesn't provide a page size, we optimistically
// continue pagination until we receive an empty page.
if (typeof requestedPageSize !== 'number') return returnedPage.length > 0;
if (requestedPageSize <= 0) return false;
return returnedPage.length >= requestedPageSize;
};

export function binarySearchByDateEqualOrNearestGreater(
array: {
created_at?: string;
Expand Down Expand Up @@ -866,13 +884,13 @@ const messagePaginationCreatedAtAround = ({
const wholePageHasOlderMessages =
!!lastPageMsg?.created_at && new Date(lastPageMsg.created_at) < createdAtAroundDate;

const pageSize = toPageSize({ requestedPageSize, returnedPage });
const requestedPageSizeNotMet =
requestedPageSize > parentSet.messages.length &&
requestedPageSize > returnedPage.length;
pageSize > parentSet.messages.length && pageSize > returnedPage.length;
const noMoreMessages =
(requestedPageSize > parentSet.messages.length ||
(pageSize > parentSet.messages.length ||
parentSet.messages.length >= returnedPage.length) &&
requestedPageSize > returnedPage.length;
pageSize > returnedPage.length;

if (wholePageHasNewerMessages) {
hasPrev = false;
Expand Down Expand Up @@ -937,10 +955,11 @@ const messagePaginationIdAround = ({
let updateHasNext = lastPageMsgIsLastInSet;

const midPoint = Math.floor(returnedPage.length / 2);
const pageSize = toPageSize({ requestedPageSize, returnedPage });
const noMoreMessages =
(requestedPageSize > parentSet.messages.length ||
(pageSize > parentSet.messages.length ||
parentSet.messages.length >= returnedPage.length) &&
requestedPageSize > returnedPage.length;
pageSize > returnedPage.length;

if (noMoreMessages) {
hasNext = hasPrev = false;
Expand Down Expand Up @@ -1009,7 +1028,7 @@ const messagePaginationLinear = ({
!messagePaginationOptions?.id_around &&
!messagePaginationOptions?.created_at_around;

const hasMore = returnedPage.length >= requestedPageSize;
const hasMore = hasMoreByPageSize({ requestedPageSize, returnedPage });

if (typeof queriedPrevMessages !== 'undefined' || containsUnrecognizedOptionsOnly) {
hasPrev = hasMore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
} from '../../../../../src/messageComposer/middleware/textComposer/mentions';
import { Channel } from '../../../../../src/channel';
import { StreamChat } from '../../../../../src/client';
import { MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY } from '../../../../../src/constants';
import type {
ChannelMemberResponse,
Mute,
Expand Down Expand Up @@ -83,6 +82,9 @@ describe('MentionsSearchSource', () => {
} as any;

channel = {
data: {
member_count: Object.keys(mockMembers).length,
},
getClient: vi.fn().mockReturnValue(client),
state: {
members: mockMembers,
Expand Down Expand Up @@ -123,18 +125,54 @@ describe('MentionsSearchSource', () => {
const source = new MentionsSearchSource(channel);
source.activate();

// Simulate more members than MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY
const manyMembers: Record<string, ChannelMemberResponse> = {};
for (let i = 0; i < MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY + 1; i++) {
manyMembers[`user${i}`] = { user: { id: `user${i}`, name: `User ${i}` } };
}
channel.state.members = manyMembers;
// Simulate a partial members cache to force remote query path.
channel.state.members = { user1: mockMembers.user1 };
channel.data = { member_count: Object.keys(mockMembers).length };

const result = await source.query('john');
expect(channel.queryMembers).toHaveBeenCalled();
expect(result.items).toHaveLength(Object.keys(mockMembers).length);
});

it('should query members when member_count is missing', async () => {
const source = new MentionsSearchSource(channel);
source.activate();
source.config.textComposerText = '@jo';
channel.data = undefined;

const result = await source.query('jo');
expect(channel.queryMembers).toHaveBeenCalledTimes(1);
expect(result.items).toHaveLength(Object.keys(mockMembers).length);
});

it('should query members when member_count does not match loaded members', async () => {
const source = new MentionsSearchSource(channel);
source.activate();
source.config.textComposerText = '@jo';
channel.data = { member_count: Object.keys(mockMembers).length + 10 };

const result = await source.query('jo');
expect(channel.queryMembers).toHaveBeenCalledTimes(1);
expect(result.items).toHaveLength(Object.keys(mockMembers).length);
});

it('should return queryMembers users without mutating channel members cache', async () => {
const source = new MentionsSearchSource(channel);
source.activate();
source.config.textComposerText = '@new';
channel.state.members = {};
channel.data = { member_count: 2 };
channel.queryMembers = vi.fn().mockResolvedValue({
members: [{ user: { id: 'new-user', name: 'New User' } }],
});

const result = await source.query('new');
expect(channel.queryMembers).toHaveBeenCalledTimes(1);
expect(result.items).toHaveLength(1);
expect(result.items[0].name).toBe('New User');
expect(channel.state.members['new-user']).toBeUndefined();
});

it('should query all app users when mentionAllAppUsers is true', async () => {
const source = new MentionsSearchSource(channel, { mentionAllAppUsers: true });
source.activate();
Expand Down Expand Up @@ -181,6 +219,24 @@ describe('MentionsSearchSource', () => {
expect(result.items[0].id).toBe('user2');
});

it('should apply mute filtering when query is executed through BaseSearchSource pipeline', async () => {
const source = new MentionsSearchSource(channel);
source.activate();
source.config.textComposerText = '@';
const mute: Mute = {
target: { id: 'user1' },
user: { id: 'currentUser' },
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
client.mutedUsers = [mute];

await source.executeQuery('');
const items = source.state.getLatestValue().items ?? [];
expect(items).toHaveLength(1);
expect(items[0].id).toBe('user2');
});

it('should preserve items in state before first query', () => {
const source = new MentionsSearchSource(channel);
source.activate();
Expand Down
9 changes: 6 additions & 3 deletions test/unit/channel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import sinon from 'sinon';
import { mockChannelQueryResponse } from './test-utils/mockChannelQueryResponse';

import { ChannelState, StreamChat } from '../../src';
import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from '../../src/constants';
import { MockOfflineDB } from './offline-support/MockOfflineDB';
import { generateUUIDv4 as uuidv4 } from '../../src/utils';

import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest';

const DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100;

describe('Channel count unread', function () {
let lastRead;
let user;
Expand Down Expand Up @@ -1933,7 +1934,7 @@ describe('Channel.query', async () => {
mock.restore();
});

it('should not update pagination for queried message set', async () => {
it('should not update pagination for queried message set when explicit limit is not met', async () => {
const client = await getClientWithUser();
const channel = client.channel('messaging', uuidv4());
const mockedChannelQueryResponse = {
Expand All @@ -1945,7 +1946,9 @@ describe('Channel.query', async () => {
};
const mock = sinon.mock(client);
mock.expects('post').returns(Promise.resolve(mockedChannelQueryResponse));
await channel.query();
await channel.query({
messages: { limit: DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE },
});
expect(channel.state.messageSets.length).to.be.equal(1);
expect(channel.state.messageSets[0].pagination).to.eql({
hasNext: false,
Expand Down
3 changes: 2 additions & 1 deletion test/unit/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { StreamChat } from '../../src/client';
import { ConnectionState } from '../../src/connection_fallback';
import { StableWSConnection } from '../../src/connection';
import { mockChannelQueryResponse } from './test-utils/mockChannelQueryResponse';
import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from '../../src/constants';

import {
describe,
Expand All @@ -23,6 +22,8 @@ import { Channel } from '../../src';
import { normalizeQuerySort } from '../../src/utils';
import { MockOfflineDB } from './offline-support/MockOfflineDB';

const DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100;

describe('StreamChat getInstance', () => {
beforeEach(() => {
delete StreamChat._instance;
Expand Down
Loading