Skip to content

Commit a51fad0

Browse files
feat: per-user channel pinning/archiving (#2555)
### 🎯 Goal Integrate channel pinning and archiving within SDK. This PR relies on: GetStream/stream-chat-js#1409 and GetStream/stream-chat-css#321
1 parent 0577ffd commit a51fad0

30 files changed

+1075
-159
lines changed

examples/vite/src/App.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ const userId = parseUserIdFromToken(userToken);
3535
const filters: ChannelFilters = {
3636
members: { $in: [userId] },
3737
type: 'messaging',
38+
archived: false,
3839
};
39-
const options: ChannelOptions = { limit: 3, presence: true, state: true };
40-
const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
40+
const options: ChannelOptions = { limit: 5, presence: true, state: true };
41+
const sort: ChannelSort = [{ pinned_at: 1 }, { last_message_at: -1 }, { updated_at: -1 }];
4142

4243
type LocalAttachmentType = Record<string, unknown>;
4344
type LocalChannelType = Record<string, unknown>;

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
"emoji-mart": "^5.4.0",
146146
"react": "^18.0.0 || ^17.0.0 || ^16.8.0",
147147
"react-dom": "^18.0.0 || ^17.0.0 || ^16.8.0",
148-
"stream-chat": "^8.46.0"
148+
"stream-chat": "^8.46.1"
149149
},
150150
"peerDependenciesMeta": {
151151
"@breezystack/lamejs": {
@@ -187,7 +187,7 @@
187187
"@semantic-release/exec": "^6.0.3",
188188
"@semantic-release/git": "^10.0.1",
189189
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
190-
"@stream-io/stream-chat-css": "^5.5.0",
190+
"@stream-io/stream-chat-css": "^5.6.0",
191191
"@testing-library/jest-dom": "^6.1.4",
192192
"@testing-library/react": "^13.1.1",
193193
"@testing-library/react-hooks": "^8.0.0",
@@ -257,7 +257,7 @@
257257
"react-dom": "^18.1.0",
258258
"react-test-renderer": "^18.1.0",
259259
"semantic-release": "^19.0.5",
260-
"stream-chat": "^8.46.0",
260+
"stream-chat": "^8.46.1",
261261
"ts-jest": "^29.1.4",
262262
"typescript": "^5.4.5"
263263
},

src/components/ChannelList/ChannelList.tsx

+40-54
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
22
import clsx from 'clsx';
33

44
import { ChannelListMessenger, ChannelListMessengerProps } from './ChannelListMessenger';
5-
import { useChannelDeletedListener } from './hooks/useChannelDeletedListener';
6-
import { useChannelHiddenListener } from './hooks/useChannelHiddenListener';
7-
import { useChannelTruncatedListener } from './hooks/useChannelTruncatedListener';
8-
import { useChannelUpdatedListener } from './hooks/useChannelUpdatedListener';
9-
import { useChannelVisibleListener } from './hooks/useChannelVisibleListener';
105
import { useConnectionRecoveredListener } from './hooks/useConnectionRecoveredListener';
11-
import { useMessageNewListener } from './hooks/useMessageNewListener';
126
import { useMobileNavigation } from './hooks/useMobileNavigation';
13-
import { useNotificationAddedToChannelListener } from './hooks/useNotificationAddedToChannelListener';
14-
import { useNotificationMessageNewListener } from './hooks/useNotificationMessageNewListener';
15-
import { useNotificationRemovedFromChannelListener } from './hooks/useNotificationRemovedFromChannelListener';
167
import { CustomQueryChannelsFn, usePaginatedChannels } from './hooks/usePaginatedChannels';
17-
import { useUserPresenceChangedListener } from './hooks/useUserPresenceChangedListener';
18-
import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUp } from './utils';
8+
import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils';
9+
1910
import { Avatar as DefaultAvatar } from '../Avatar';
2011
import { ChannelPreview, ChannelPreviewUIComponentProps } from '../ChannelPreview/ChannelPreview';
2112
import {
@@ -37,6 +28,7 @@ import type { Channel, ChannelFilters, ChannelOptions, ChannelSort, Event } from
3728
import type { ChannelAvatarProps } from '../Avatar';
3829
import type { TranslationContextValue } from '../../context/TranslationContext';
3930
import type { DefaultStreamChatGenerics, PaginatorProps } from '../../types/types';
31+
import { useChannelListShape, usePrepareShapeHandlers } from './hooks/useChannelListShape';
4032

4133
const DEFAULT_FILTERS = {};
4234
const DEFAULT_OPTIONS = {};
@@ -62,6 +54,7 @@ export type ChannelListProps<
6254
) => Array<Channel<StreamChatGenerics>>;
6355
/** Custom UI component to display search results, defaults to and accepts same props as: [ChannelSearch](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelSearch/ChannelSearch.tsx) */
6456
ChannelSearch?: React.ComponentType<ChannelSearchProps<StreamChatGenerics>>;
57+
// FIXME: how is this even legal (WHY IS IT STRING?!)
6558
/** Set a channel (with this ID) to active and manually move it to the top of the list */
6659
customActiveChannel?: string;
6760
/** Custom function that handles the channel pagination. Has to build query filters, sort and options and query and append channels to the current channels state and update the hasNext pagination flag after each query. */
@@ -160,26 +153,24 @@ export type ChannelListProps<
160153
watchers?: { limit?: number; offset?: number };
161154
};
162155

163-
const UnMemoizedChannelList = <
164-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
165-
>(
166-
props: ChannelListProps<StreamChatGenerics>,
156+
const UnMemoizedChannelList = <SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(
157+
props: ChannelListProps<SCG>,
167158
) => {
168159
const {
169160
additionalChannelSearchProps,
170161
Avatar = DefaultAvatar,
171-
allowNewMessagesFromUnfilteredChannels,
162+
allowNewMessagesFromUnfilteredChannels = true,
172163
channelRenderFilterFn,
173164
ChannelSearch = DefaultChannelSearch,
174165
customActiveChannel,
175166
customQueryChannels,
176167
EmptyStateIndicator = DefaultEmptyStateIndicator,
177-
filters,
168+
filters = {},
178169
getLatestMessagePreview,
179170
LoadingErrorIndicator = NullComponent,
180171
LoadingIndicator = LoadingChannels,
181172
List = ChannelListMessenger,
182-
lockChannelOrder,
173+
lockChannelOrder = false,
183174
onAddedToChannel,
184175
onChannelDeleted,
185176
onChannelHidden,
@@ -211,7 +202,7 @@ const UnMemoizedChannelList = <
211202
setActiveChannel,
212203
theme,
213204
useImageFlagEmojisOnWindows,
214-
} = useChatContext<StreamChatGenerics>('ChannelList');
205+
} = useChatContext<SCG>('ChannelList');
215206

216207
const channelListRef = useRef<HTMLDivElement>(null);
217208
const [channelUpdateCount, setChannelUpdateCount] = useState(0);
@@ -221,14 +212,15 @@ const UnMemoizedChannelList = <
221212
* If customActiveChannel prop is absent, then set the first channel in list as active channel.
222213
*/
223214
const activeChannelHandler = async (
224-
channels: Array<Channel<StreamChatGenerics>>,
225-
setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>,
215+
channels: Array<Channel<SCG>>,
216+
setChannels: React.Dispatch<React.SetStateAction<Array<Channel<SCG>>>>,
226217
) => {
227218
if (!channels.length || channels.length > (options?.limit || MAX_QUERY_CHANNELS_LIMIT)) {
228219
return;
229220
}
230221

231222
if (customActiveChannel) {
223+
// FIXME: this is wrong...
232224
let customActiveChannelObject = channels.find((chan) => chan.id === customActiveChannel);
233225

234226
if (!customActiveChannelObject) {
@@ -239,10 +231,10 @@ const UnMemoizedChannelList = <
239231
if (customActiveChannelObject) {
240232
setActiveChannel(customActiveChannelObject, watchers);
241233

242-
const newChannels = moveChannelUp({
243-
activeChannel: customActiveChannelObject,
234+
const newChannels = moveChannelUpwards({
244235
channels,
245-
cid: customActiveChannelObject.cid,
236+
channelToMove: customActiveChannelObject,
237+
sort,
246238
});
247239

248240
setChannels(newChannels);
@@ -260,16 +252,11 @@ const UnMemoizedChannelList = <
260252
* For some events, inner properties on the channel will update but the shallow comparison will not
261253
* force a re-render. Incrementing this dummy variable ensures the channel previews update.
262254
*/
263-
const forceUpdate = useCallback(() => setChannelUpdateCount((count) => count + 1), [
264-
setChannelUpdateCount,
265-
]);
255+
const forceUpdate = useCallback(() => setChannelUpdateCount((count) => count + 1), []);
266256

267257
const onSearch = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
268-
if (!event.target.value) {
269-
setSearchActive(false);
270-
} else {
271-
setSearchActive(true);
272-
}
258+
setSearchActive(!!event.target.value);
259+
273260
additionalChannelSearchProps?.onSearch?.(event);
274261
// eslint-disable-next-line react-hooks/exhaustive-deps
275262
}, []);
@@ -294,33 +281,32 @@ const UnMemoizedChannelList = <
294281

295282
useMobileNavigation(channelListRef, navOpen, closeMobileNav);
296283

297-
useMessageNewListener(
298-
setChannels,
299-
onMessageNewHandler,
300-
lockChannelOrder,
284+
const { customHandler, defaultHandler } = usePrepareShapeHandlers<SCG>({
301285
allowNewMessagesFromUnfilteredChannels,
302-
);
303-
useNotificationMessageNewListener(
304-
setChannels,
286+
filters,
287+
lockChannelOrder,
288+
onAddedToChannel,
289+
onChannelDeleted,
290+
onChannelHidden,
291+
onChannelTruncated,
292+
onChannelUpdated,
293+
onChannelVisible,
305294
onMessageNew,
306-
allowNewMessagesFromUnfilteredChannels,
307-
);
308-
useNotificationAddedToChannelListener(
295+
onMessageNewHandler,
296+
onRemovedFromChannel,
309297
setChannels,
310-
onAddedToChannel,
311-
allowNewMessagesFromUnfilteredChannels,
312-
);
313-
useNotificationRemovedFromChannelListener(setChannels, onRemovedFromChannel);
314-
useChannelDeletedListener(setChannels, onChannelDeleted);
315-
useChannelHiddenListener(setChannels, onChannelHidden);
316-
useChannelVisibleListener(setChannels, onChannelVisible);
317-
useChannelTruncatedListener(setChannels, onChannelTruncated, forceUpdate);
318-
useChannelUpdatedListener(setChannels, onChannelUpdated, forceUpdate);
298+
sort,
299+
// TODO: implement
300+
// customHandleChannelListShape
301+
});
302+
303+
useChannelListShape<SCG>(customHandler ?? defaultHandler);
304+
305+
// TODO: maybe move this too
319306
useConnectionRecoveredListener(forceUpdate);
320-
useUserPresenceChangedListener(setChannels);
321307

322308
useEffect(() => {
323-
const handleEvent = (event: Event<StreamChatGenerics>) => {
309+
const handleEvent = (event: Event<SCG>) => {
324310
if (event.cid === channel?.cid) {
325311
setActiveChannel();
326312
}
@@ -336,7 +322,7 @@ const UnMemoizedChannelList = <
336322
// eslint-disable-next-line react-hooks/exhaustive-deps
337323
}, [channel?.cid]);
338324

339-
const renderChannel = (item: Channel<StreamChatGenerics>) => {
325+
const renderChannel = (item: Channel<SCG>) => {
340326
const previewProps = {
341327
activeChannel: channel,
342328
Avatar,

src/components/ChannelList/__tests__/ChannelList.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
queryChannelsApi,
2727
queryUsersApi,
2828
useMockedApis,
29-
} from 'mock-builders';
29+
} from '../../../mock-builders';
3030

3131
import { Chat } from '../../Chat';
3232
import { ChannelList } from '../ChannelList';

src/components/ChannelList/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './useNotificationMessageNewListener';
1111
export * from './useNotificationRemovedFromChannelListener';
1212
export * from './usePaginatedChannels';
1313
export * from './useUserPresenceChangedListener';
14+
export * from './useChannelMembershipState';

0 commit comments

Comments
 (0)