Skip to content

Commit 56d807d

Browse files
Initial commit
1 parent 0577ffd commit 56d807d

File tree

5 files changed

+159
-34
lines changed

5 files changed

+159
-34
lines changed

src/components/ChannelList/ChannelList.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { useNotificationMessageNewListener } from './hooks/useNotificationMessag
1515
import { useNotificationRemovedFromChannelListener } from './hooks/useNotificationRemovedFromChannelListener';
1616
import { CustomQueryChannelsFn, usePaginatedChannels } from './hooks/usePaginatedChannels';
1717
import { useUserPresenceChangedListener } from './hooks/useUserPresenceChangedListener';
18-
import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUp } from './utils';
18+
import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils';
19+
1920
import { Avatar as DefaultAvatar } from '../Avatar';
2021
import { ChannelPreview, ChannelPreviewUIComponentProps } from '../ChannelPreview/ChannelPreview';
2122
import {
@@ -43,7 +44,7 @@ const DEFAULT_OPTIONS = {};
4344
const DEFAULT_SORT = {};
4445

4546
export type ChannelListProps<
46-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
47+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
4748
> = {
4849
/** Additional props for underlying ChannelSearch component and channel search controller, [available props](https://getstream.io/chat/docs/sdk/react/utility-components/channel_search/#props) */
4950
additionalChannelSearchProps?: Omit<ChannelSearchProps<StreamChatGenerics>, 'setChannels'>;
@@ -62,6 +63,7 @@ export type ChannelListProps<
6263
) => Array<Channel<StreamChatGenerics>>;
6364
/** 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) */
6465
ChannelSearch?: React.ComponentType<ChannelSearchProps<StreamChatGenerics>>;
66+
// FIXME: how is this even legal (WHY IS IT STRING?!)
6567
/** Set a channel (with this ID) to active and manually move it to the top of the list */
6668
customActiveChannel?: string;
6769
/** 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. */
@@ -161,7 +163,7 @@ export type ChannelListProps<
161163
};
162164

163165
const UnMemoizedChannelList = <
164-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
166+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
165167
>(
166168
props: ChannelListProps<StreamChatGenerics>,
167169
) => {
@@ -229,6 +231,7 @@ const UnMemoizedChannelList = <
229231
}
230232

231233
if (customActiveChannel) {
234+
// FIXME: this is wrong...
232235
let customActiveChannelObject = channels.find((chan) => chan.id === customActiveChannel);
233236

234237
if (!customActiveChannelObject) {
@@ -239,10 +242,12 @@ const UnMemoizedChannelList = <
239242
if (customActiveChannelObject) {
240243
setActiveChannel(customActiveChannelObject, watchers);
241244

242-
const newChannels = moveChannelUp({
243-
activeChannel: customActiveChannelObject,
245+
const newChannels = moveChannelUpwards({
244246
channels,
245-
cid: customActiveChannelObject.cid,
247+
channelToMove: customActiveChannelObject,
248+
// TODO: adjust acordingly (based on sort)
249+
considerPinnedChannels: false,
250+
userId: client.userID!,
246251
});
247252

248253
setChannels(newChannels);
@@ -260,9 +265,10 @@ const UnMemoizedChannelList = <
260265
* For some events, inner properties on the channel will update but the shallow comparison will not
261266
* force a re-render. Incrementing this dummy variable ensures the channel previews update.
262267
*/
263-
const forceUpdate = useCallback(() => setChannelUpdateCount((count) => count + 1), [
264-
setChannelUpdateCount,
265-
]);
268+
const forceUpdate = useCallback(
269+
() => setChannelUpdateCount((count) => count + 1),
270+
[setChannelUpdateCount],
271+
);
266272

267273
const onSearch = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
268274
if (!event.target.value) {
@@ -299,6 +305,8 @@ const UnMemoizedChannelList = <
299305
onMessageNewHandler,
300306
lockChannelOrder,
301307
allowNewMessagesFromUnfilteredChannels,
308+
// TODO: adjust accordingly (consider sort option)
309+
false,
302310
);
303311
useNotificationMessageNewListener(
304312
setChannels,

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

Lines changed: 1 addition & 1 deletion
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/useMessageNewListener.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,82 @@
11
import { useEffect } from 'react';
2-
import uniqBy from 'lodash.uniqby';
3-
4-
import { moveChannelUp } from '../utils';
2+
import type { Dispatch, SetStateAction } from 'react';
53

64
import { useChatContext } from '../../../context/ChatContext';
75

8-
import type { Channel, Event } from 'stream-chat';
6+
import type { Channel, Event, ExtendableGenerics } from 'stream-chat';
97

108
import type { DefaultStreamChatGenerics } from '../../../types/types';
9+
import { moveChannelUpwards } from '../utils';
10+
11+
export const isChannelPinned = <SCG extends ExtendableGenerics>({
12+
channel,
13+
userId,
14+
}: {
15+
userId: string;
16+
channel?: Channel<SCG>;
17+
}) => {
18+
if (!channel) return false;
19+
20+
const member = channel.state.members[userId];
21+
22+
return !!member?.pinned_at;
23+
};
1124

1225
export const useMessageNewListener = <
13-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
26+
SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
1427
>(
15-
setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>,
28+
setChannels: Dispatch<SetStateAction<Array<Channel<SCG>>>>,
1629
customHandler?: (
17-
setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>,
18-
event: Event<StreamChatGenerics>,
30+
setChannels: Dispatch<SetStateAction<Array<Channel<SCG>>>>,
31+
event: Event<SCG>,
1932
) => void,
2033
lockChannelOrder = false,
2134
allowNewMessagesFromUnfilteredChannels = true,
35+
considerPinnedChannels = false, // automatically set to true by checking sorting options (must include {pinned_at: -1/1})
2236
) => {
23-
const { client } = useChatContext<StreamChatGenerics>('useMessageNewListener');
37+
const { client } = useChatContext<SCG>('useMessageNewListener');
2438

2539
useEffect(() => {
26-
const handleEvent = (event: Event<StreamChatGenerics>) => {
40+
const handleEvent = (event: Event<SCG>) => {
2741
if (customHandler && typeof customHandler === 'function') {
2842
customHandler(setChannels, event);
2943
} else {
3044
setChannels((channels) => {
31-
const channelInList = channels.filter((channel) => channel.cid === event.cid).length > 0;
45+
const targetChannelIndex = channels.findIndex((channel) => channel.cid === event.cid);
46+
const targetChannelExistsWithinList = targetChannelIndex >= 0;
47+
48+
const isTargetChannelPinned = isChannelPinned({
49+
channel: channels[targetChannelIndex],
50+
userId: client.userID!,
51+
});
3252

33-
if (!channelInList && allowNewMessagesFromUnfilteredChannels && event.channel_type) {
34-
const channel = client.channel(event.channel_type, event.channel_id);
35-
return uniqBy([channel, ...channels], 'cid');
53+
if (
54+
// target channel is pinned
55+
(isTargetChannelPinned && considerPinnedChannels) ||
56+
// list order is locked
57+
lockChannelOrder ||
58+
// target channel is not within the loaded list and loading from cache is disallowed
59+
(!targetChannelExistsWithinList && !allowNewMessagesFromUnfilteredChannels)
60+
) {
61+
return channels;
3662
}
3763

38-
if (!lockChannelOrder) return moveChannelUp({ channels, cid: event.cid || '' });
64+
// we either have the channel to move or we pull it from the cache (or instantiate) if it's allowed
65+
const channelToMove: Channel<SCG> | null =
66+
channels[targetChannelIndex] ??
67+
(allowNewMessagesFromUnfilteredChannels && event.channel_type
68+
? client.channel(event.channel_type, event.channel_id)
69+
: null);
70+
71+
if (channelToMove) {
72+
return moveChannelUpwards({
73+
channels,
74+
channelToMove,
75+
channelToMoveIndexWithinChannels: targetChannelIndex,
76+
considerPinnedChannels,
77+
userId: client.userID!,
78+
});
79+
}
3980

4081
return channels;
4182
});
@@ -50,6 +91,7 @@ export const useMessageNewListener = <
5091
}, [
5192
allowNewMessagesFromUnfilteredChannels,
5293
client,
94+
considerPinnedChannels,
5395
customHandler,
5496
lockChannelOrder,
5597
setChannels,
Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,51 @@
11
import type { Channel } from 'stream-chat';
22
import uniqBy from 'lodash.uniqby';
33

4+
import { isChannelPinned } from './hooks';
5+
46
import type { DefaultStreamChatGenerics } from '../../types/types';
57

68
export const MAX_QUERY_CHANNELS_LIMIT = 30;
79

8-
type MoveChannelUpParams<
9-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
10-
> = {
11-
channels: Array<Channel<StreamChatGenerics>>;
10+
type MoveChannelUpParams<SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
11+
channels: Array<Channel<SCG>>;
1212
cid: string;
13-
activeChannel?: Channel<StreamChatGenerics>;
13+
userId: string;
14+
activeChannel?: Channel<SCG>;
15+
channelIndexWithinChannels?: number;
16+
considerPinnedChannels?: boolean;
1417
};
1518

16-
export const moveChannelUp = <
17-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
18-
>({
19+
type MoveChannelUpwardsParams<SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
20+
channels: Array<Channel<SCG>>;
21+
channelToMove: Channel<SCG>;
22+
/**
23+
* If the index of the channel within `channels` list which is being moved upwards
24+
* (`channelToMove`) is known, you can supply it to skip extra calculation.
25+
*/
26+
channelToMoveIndexWithinChannels?: number;
27+
/**
28+
* Pinned channels should not move within the list based on recent activity, channels which
29+
* receive messages and are not pinned should move upwards but only under the last pinned channel
30+
* in the list. Property defaults to `false` and should be calculated based on existence of
31+
* the `pinned_at` sort option.
32+
*/
33+
considerPinnedChannels?: boolean;
34+
/**
35+
* If `considerPinnedChannels` is set to `true`, then `userId` should be supplied - without it the
36+
* pinned channels won't be considered.
37+
*/
38+
userId?: string;
39+
};
40+
41+
/**
42+
* @deprecated
43+
*/
44+
export const moveChannelUp = <SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({
1945
activeChannel,
2046
channels,
2147
cid,
22-
}: MoveChannelUpParams<StreamChatGenerics>) => {
48+
}: MoveChannelUpParams<SCG>) => {
2349
// get index of channel to move up
2450
const channelIndex = channels.findIndex((channel) => channel.cid === cid);
2551

@@ -30,3 +56,50 @@ export const moveChannelUp = <
3056

3157
return uniqBy([channel, ...channels], 'cid');
3258
};
59+
60+
export const moveChannelUpwards = <
61+
SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
62+
>({
63+
channels,
64+
channelToMove,
65+
channelToMoveIndexWithinChannels,
66+
considerPinnedChannels = false,
67+
userId,
68+
}: MoveChannelUpwardsParams<SCG>) => {
69+
// get index of channel to move up
70+
const targetChannelIndex =
71+
channelToMoveIndexWithinChannels ??
72+
channels.findIndex((channel) => channel.cid === channelToMove.cid);
73+
74+
const targetChannelExistsWithinList = targetChannelIndex >= 0;
75+
const targetChannelAlreadyAtTheTop = targetChannelIndex === 0;
76+
77+
if (targetChannelAlreadyAtTheTop) return channels;
78+
79+
// as position of pinned channels has to stay unchanged, we need to
80+
// find last pinned channel in the list to move the target channel after
81+
let lastPinIndex: number | null = null;
82+
if (considerPinnedChannels && userId) {
83+
for (const c of channels) {
84+
if (!isChannelPinned({ channel: c, userId })) break;
85+
86+
if (typeof lastPinIndex === 'number') {
87+
lastPinIndex++;
88+
} else {
89+
lastPinIndex = 0;
90+
}
91+
}
92+
}
93+
94+
const newChannels = [...channels];
95+
96+
// target channel index is known, remove it from the list
97+
if (targetChannelExistsWithinList) {
98+
newChannels.splice(targetChannelIndex, 1);
99+
}
100+
101+
// re-insert it at the new place (to specific index if pinned channels are considered)
102+
newChannels.splice(typeof lastPinIndex === 'number' ? lastPinIndex + 1 : 0, 0, channelToMove);
103+
104+
return newChannels;
105+
};

src/mock-builders/event/messageNew.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export default (client, newMessage, channel = {}) => {
22
client.dispatchEvent({
33
channel,
4+
channel_id: channel.id,
5+
channel_type: channel.type,
46
cid: channel.cid,
57
message: newMessage,
68
type: 'message.new',

0 commit comments

Comments
 (0)