Skip to content
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
18 changes: 9 additions & 9 deletions examples/ExpoMessaging/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { Alert, Image, Pressable, StyleSheet, View } from 'react-native';
import { ChannelList, SqliteClient } from 'stream-chat-expo';
import { useCallback, useContext, useMemo } from 'react';
import { Stack, useRouter } from 'expo-router';
import { ChannelSort } from 'stream-chat';
import { AppContext } from '../context/AppContext';
import { useUserContext } from '@/context/UserContext';
import { getInitialsOfName } from '@/utils/getInitialsOfName';

const sort: ChannelSort = { last_updated: -1 };
const options = {
const baseOptions = {
predefined_filter: 'basic_channel_list_filter',
state: true,
watch: true,
};
Expand Down Expand Up @@ -46,12 +45,15 @@ const LogoutButton = () => {

export default function ChannelListScreen() {
const { user } = useUserContext();
const filters = useMemo(
const userId = user?.id || '';
const options = useMemo(
() => ({
members: { $in: [user?.id as string] },
type: 'messaging',
...baseOptions,
filter_values: {
user_id: userId,
},
}),
[user?.id],
[userId],
);
const router = useRouter();
const { setChannel } = useContext(AppContext);
Expand All @@ -63,13 +65,11 @@ export default function ChannelListScreen() {
/>

<ChannelList
filters={filters}
onSelect={(channel) => {
setChannel(channel);
router.push(`/channel/${channel.cid}`);
}}
options={options}
sort={sort}
/>
</View>
);
Expand Down
28 changes: 8 additions & 20 deletions examples/SampleApp/src/screens/ChannelListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { MessageSearchList } from '../components/MessageSearch/MessageSearchList
import { useAppContext } from '../context/AppContext';
import { usePaginatedSearchedMessages } from '../hooks/usePaginatedSearchedMessages';

import type { ChannelSort } from 'stream-chat';
import { useStreamChatContext } from '../context/StreamChatContext';
import { Search } from '../icons/Search';
import { ChannelInfo } from '../icons/ChannelInfo.tsx';
Expand Down Expand Up @@ -60,18 +59,12 @@ const styles = StyleSheet.create({
},
});

const baseFilters = {
archived: false,
type: 'messaging',
};

const sort: ChannelSort = [{ pinned_at: -1 }, { last_message_at: -1 }, { updated_at: -1 }];

const options = {
const baseOptions = {
presence: true,
state: true,
watch: true,
message_limit: 25,
predefined_filter: 'basic_channel_list_filter',
};

export const ChannelListScreen: React.FC = () => {
Expand All @@ -91,15 +84,12 @@ export const ChannelListScreen: React.FC = () => {
usePaginatedSearchedMessages(searchQuery);

const chatClientUserId = chatClient?.user?.id || '';
const filters = useMemo(
() => ({
...baseFilters,
members: {
$in: [chatClientUserId],
},
}),
[chatClientUserId],
);
const options = useMemo(() => ({
...baseOptions,
filter_values: {
user_id: chatClientUserId,
}
}), [chatClientUserId])

useScrollToTop(scrollRef as RefObject<FlatList<Channel>>);

Expand Down Expand Up @@ -248,13 +238,11 @@ export const ChannelListScreen: React.FC = () => {
<View style={[styles.channelListContainer, { opacity: searchQuery ? 0 : 1 }]}>
<ChannelList
additionalFlatListProps={additionalFlatListProps}
filters={filters}
maxUnreadCount={99}
onSelect={onSelect}
options={options}
setFlatListRef={setScrollRef}
getChannelActionItems={getChannelActionItems}
sort={sort}
/>
</View>
</View>
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^2.0.0",
"stream-chat": "^9.43.2",
"stream-chat": "^9.44.1",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
Expand Down
68 changes: 68 additions & 0 deletions package/src/components/ChannelList/__tests__/ChannelList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,74 @@ describe('ChannelList', () => {
});
});

it('should re-query channels when predefined filter options change', async () => {
const queryChannelsSpy = jest.spyOn(chatClient, 'queryChannels');
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);

render(
<Chat client={chatClient}>
<WithComponents overrides={{ ChannelPreview: ChannelPreviewComponent }}>
<ChannelList
{...props}
options={{
filter_values: { user_id: 'dan' },
predefined_filter: 'user_messaging',
}}
/>
</WithComponents>
</Chat>,
);

await waitFor(() => {
expect(screen.getByTestId(testChannel1.channel.id)).toBeTruthy();
});

expect(queryChannelsSpy).toHaveBeenNthCalledWith(
1,
{},
expect.anything(),
expect.objectContaining({
filter_values: { user_id: 'dan' },
offset: 0,
predefined_filter: 'user_messaging',
}),
expect.anything(),
);

useMockedApis(chatClient, [queryChannelsApi([testChannel2])]);

screen.rerender(
<Chat client={chatClient}>
<WithComponents overrides={{ ChannelPreview: ChannelPreviewComponent }}>
<ChannelList
{...props}
options={{
filter_values: { user_id: 'sara' },
predefined_filter: 'user_messaging',
}}
/>
</WithComponents>
</Chat>,
);

await waitFor(() => {
expect(queryChannelsSpy).toHaveBeenCalledTimes(2);
expect(screen.getByTestId(testChannel2.channel.id)).toBeTruthy();
});

expect(queryChannelsSpy).toHaveBeenNthCalledWith(
2,
{},
expect.anything(),
expect.objectContaining({
filter_values: { user_id: 'sara' },
offset: 0,
predefined_filter: 'user_messaging',
}),
expect.anything(),
);
});

it('should update if filters are updated while awaiting api call', async () => {
const deferredCallForStaleFilter = new DeferredPromise();
const deferredCallForFreshFilter = new DeferredPromise();
Expand Down
14 changes: 8 additions & 6 deletions package/src/components/ChannelList/hooks/usePaginatedChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const usePaginatedChannels = ({
const hasNextPage = pagination?.hasNext;

const filtersRef = useRef<typeof filters | null>(null);
const optionsRef = useRef<typeof options | null>(null);
const sortRef = useRef<typeof sort | null>(null);
const activeRequestId = useRef<number>(0);
const isQueryingRef = useRef(false);
Expand All @@ -69,10 +70,9 @@ export const usePaginatedChannels = ({
queryType === 'loadChannels' ||
queryType === 'refresh' ||
queryType === 'backgroundRefresh' ||
[
JSON.stringify(filtersRef.current) !== JSON.stringify(filters),
JSON.stringify(sortRef.current) !== JSON.stringify(sort),
].some(Boolean);
JSON.stringify(filtersRef.current) !== JSON.stringify(filters) ||
JSON.stringify(optionsRef.current) !== JSON.stringify(options) ||
JSON.stringify(sortRef.current) !== JSON.stringify(sort);

const isQueryStale = () => !isMountedRef || activeRequestId.current !== currentRequestId;

Expand All @@ -87,6 +87,7 @@ export const usePaginatedChannels = ({
}

filtersRef.current = filters;
optionsRef.current = options;
sortRef.current = sort;
isQueryingRef.current = true;
activeRequestId.current++;
Expand Down Expand Up @@ -146,7 +147,7 @@ export const usePaginatedChannels = ({
};

/**
* Equality check using stringified filters/sort ensure that we don't make un-necessary queryChannels api calls
* Equality check using stringified filters/options/sort ensure that we don't make un-necessary queryChannels api calls
* for the scenario:
*
* <ChannelList
Expand All @@ -161,6 +162,7 @@ export const usePaginatedChannels = ({
* in return will trigger useEffect. To avoid this, we can add a value check.
*/
const filterStr = useMemo(() => JSON.stringify(filters), [filters]);
const optionsStr = useMemo(() => JSON.stringify(options), [options]);
const sortStr = useMemo(() => JSON.stringify(sort), [sort]);

useEffect(() => {
Expand All @@ -178,7 +180,7 @@ export const usePaginatedChannels = ({

return () => listener?.unsubscribe?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterStr, sortStr, channelManager]);
}, [filterStr, optionsStr, sortStr, channelManager]);

return {
channelListInitialized,
Expand Down
4 changes: 2 additions & 2 deletions package/src/store/OfflineDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export class OfflineDB extends AbstractOfflineDB {
api.getChannels({ channelIds: cids, currentUserId: userId });

// TODO: Rename currentUserId -> userId in the next major version as it is technically breaking.
getChannelsForQuery = ({ userId, filters, sort }: DBGetChannelsForQueryType) =>
api.getChannelsForFilterSort({ currentUserId: userId, filters, sort });
getChannelsForQuery = ({ userId, filters, options, sort }: DBGetChannelsForQueryType) =>
api.getChannelsForFilterSort({ currentUserId: userId, filters, options, sort });

getAllChannelCids = api.getAllChannelIds;

Expand Down
55 changes: 55 additions & 0 deletions package/src/store/apis/__tests__/channelQueryCids.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BetterSqlite } from '../../../test-utils/BetterSqlite';
import { SqliteClient } from '../../SqliteClient';
import { selectChannelIdsForFilterSort } from '../queries/selectChannelIdsForFilterSort';
import { upsertCidsForQuery } from '../upsertCidsForQuery';

describe('channel query cids', () => {
beforeEach(async () => {
await SqliteClient.initializeDatabase();
await BetterSqlite.openDB();
});

afterEach(() => {
BetterSqlite.dropAllTables();
BetterSqlite.closeDB();
jest.clearAllMocks();
});

it('stores separate cid lists for predefined filter queries with the same filters and sort', async () => {
await upsertCidsForQuery({
cids: ['messaging:channel-1'],
filters: {},
options: {
predefined_filter: 'user_messaging',
},
sort: {},
});
await upsertCidsForQuery({
cids: ['messaging:channel-2'],
filters: {},
options: {
predefined_filter: 'team_channels',
},
sort: {},
});

await expect(
selectChannelIdsForFilterSort({
filters: {},
options: {
predefined_filter: 'user_messaging',
},
sort: {},
}),
).resolves.toEqual(['messaging:channel-1']);
await expect(
selectChannelIdsForFilterSort({
filters: {},
options: {
predefined_filter: 'team_channels',
},
sort: {},
}),
).resolves.toEqual(['messaging:channel-2']);
});
});
14 changes: 9 additions & 5 deletions package/src/store/apis/getChannelsForFilterSort.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChannelAPIResponse, ChannelFilters, ChannelSort } from 'stream-chat';
import type { ChannelAPIResponse, ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';

import { getChannels } from './getChannels';
import { selectChannelIdsForFilterSort } from './queries/selectChannelIdsForFilterSort';
Expand All @@ -18,20 +18,24 @@ import { SqliteClient } from '../SqliteClient';
export const getChannelsForFilterSort = async ({
currentUserId,
filters,
options,
sort,
}: {
currentUserId: string;
filters?: ChannelFilters;
options?: ChannelOptions;
sort?: ChannelSort;
}): Promise<Omit<ChannelAPIResponse, 'duration'>[] | null> => {
if (!filters && !sort) {
console.warn('Please provide the query (filters/sort) to fetch channels from DB');
if (!filters && !sort && !options?.predefined_filter) {
console.warn(
'Please provide the query (filters/sort/options.predefined_filter) to fetch channels from the DB.',
);
return null;
}

SqliteClient.logger?.('info', 'getChannelsForFilterSort', { filters, sort });
SqliteClient.logger?.('info', 'getChannelsForFilterSort', { filters, options, sort });

const channelIds = await selectChannelIdsForFilterSort({ filters, sort });
const channelIds = await selectChannelIdsForFilterSort({ filters, options, sort });

if (!channelIds) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChannelFilters, ChannelSort } from 'stream-chat';
import type { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';

import { createSelectQuery } from '../../sqlite-utils/createSelectQuery';
import { SqliteClient } from '../../SqliteClient';
Expand All @@ -17,12 +17,14 @@ import { convertFilterSortToQuery } from '../utils/convertFilterSortToQuery';

export const selectChannelIdsForFilterSort = async ({
filters,
options,
sort,
}: {
filters?: ChannelFilters;
options?: ChannelOptions;
sort?: ChannelSort;
}): Promise<string[] | null> => {
const query = convertFilterSortToQuery({ filters, sort });
const query = convertFilterSortToQuery({ filters, options, sort });

SqliteClient.logger?.('info', 'selectChannelIdsForFilterSort', {
query,
Expand Down
Loading