Skip to content

Commit f3c1e08

Browse files
committed
feat: channel pinning and archiving events handler improvements
1 parent 82cee02 commit f3c1e08

File tree

4 files changed

+115
-8
lines changed

4 files changed

+115
-8
lines changed

package/src/components/ChannelList/ChannelList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,15 @@ export type ChannelListProps<
166166
* @param event An [Event object](https://getstream.io/chat/docs/event_object) corresponding to `message.new` event
167167
* @param considerArchivedChannels If set to true, archived channels will be considered while updating the list of channels
168168
* @param filters Channel filters
169+
* @param sort Channel sort options
169170
* @overrideType Function
170171
* */
171172
onNewMessage?: (
172173
lockChannelOrder: boolean,
173174
setChannels: React.Dispatch<React.SetStateAction<Channel<StreamChatGenerics>[] | null>>,
174175
event: Event<StreamChatGenerics>,
175176
filters?: ChannelFilters<StreamChatGenerics>,
177+
sort?: ChannelSort<StreamChatGenerics>,
176178
) => void;
177179
/**
178180
* Override the default listener/handler for event `notification.message_new`
@@ -354,6 +356,7 @@ export const ChannelList = <
354356
onNewMessage,
355357
setChannels,
356358
filters,
359+
sort,
357360
});
358361

359362
useNewMessageNotification({

package/src/components/ChannelList/hooks/listeners/useChannelMemberUpdated.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import type { Channel, ChannelFilters, ChannelSort, Event } from 'stream-chat';
55
import { useChatContext } from '../../../../contexts/chatContext/ChatContext';
66

77
import type { DefaultStreamChatGenerics } from '../../../../types/types';
8+
import {
9+
findLastPinnedChannelIndex,
10+
findPinnedAtSortOrder,
11+
shouldConsiderPinnedChannels,
12+
} from '../utils';
813

914
type Parameters<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> =
1015
{
@@ -44,6 +49,9 @@ export const useChannelMemberUpdated = <
4449
const channelType = event.channel_type;
4550
const channelId = event.channel_id;
4651

52+
const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
53+
const pinnedAtSort = findPinnedAtSortOrder({ sort });
54+
4755
setChannels((currentChannels) => {
4856
if (!currentChannels) return currentChannels;
4957

@@ -52,7 +60,7 @@ export const useChannelMemberUpdated = <
5260
const targetChannelIndex = currentChannels.indexOf(targetChannel);
5361
const targetChannelExistsWithinList = targetChannelIndex >= 0;
5462

55-
if (lockChannelOrder) {
63+
if (!considerPinnedChannels || lockChannelOrder) {
5664
return currentChannels;
5765
}
5866

@@ -69,7 +77,24 @@ export const useChannelMemberUpdated = <
6977
) {
7078
return newChannels;
7179
}
72-
return currentChannels;
80+
81+
// handle pinning
82+
let lastPinnedChannelIndex: number | null = null;
83+
84+
if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !member.pinned_at)) {
85+
lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
86+
}
87+
const newTargetChannelIndex =
88+
typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0;
89+
90+
// skip re-render if the position of the channel does not change
91+
if (currentChannels[newTargetChannelIndex] === targetChannel) {
92+
return currentChannels;
93+
}
94+
95+
newChannels.splice(newTargetChannelIndex, 0, targetChannel);
96+
97+
return newChannels;
7398
});
7499
}
75100
};

package/src/components/ChannelList/hooks/listeners/useNewMessage.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useEffect } from 'react';
22

3-
import type { Channel, ChannelFilters, Event } from 'stream-chat';
3+
import type { Channel, ChannelFilters, ChannelSort, Event } from 'stream-chat';
44

55
import { useChatContext } from '../../../../contexts/chatContext/ChatContext';
66

77
import type { DefaultStreamChatGenerics } from '../../../../types/types';
88
import { moveChannelUp } from '../../utils';
9-
import { isChannelArchived } from '../utils';
9+
import { isChannelArchived, isChannelPinned, shouldConsiderPinnedChannels } from '../utils';
1010

1111
type Parameters<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> =
1212
{
@@ -17,8 +17,10 @@ type Parameters<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultSt
1717
setChannels: React.Dispatch<React.SetStateAction<Channel<StreamChatGenerics>[] | null>>,
1818
event: Event<StreamChatGenerics>,
1919
filters?: ChannelFilters<StreamChatGenerics>,
20+
sort?: ChannelSort<StreamChatGenerics>,
2021
) => void;
2122
filters?: ChannelFilters<StreamChatGenerics>;
23+
sort?: ChannelSort<StreamChatGenerics>;
2224
};
2325

2426
export const useNewMessage = <
@@ -28,13 +30,14 @@ export const useNewMessage = <
2830
onNewMessage,
2931
setChannels,
3032
filters,
33+
sort,
3134
}: Parameters<StreamChatGenerics>) => {
3235
const { client } = useChatContext<StreamChatGenerics>();
3336

3437
useEffect(() => {
3538
const handleEvent = (event: Event<StreamChatGenerics>) => {
3639
if (typeof onNewMessage === 'function') {
37-
onNewMessage(lockChannelOrder, setChannels, event, filters);
40+
onNewMessage(lockChannelOrder, setChannels, event, filters, sort);
3841
} else {
3942
setChannels((channels) => {
4043
if (!channels) return channels;
@@ -45,11 +48,19 @@ export const useNewMessage = <
4548
const targetChannel = channels[targetChannelIndex];
4649

4750
const isTargetChannelArchived = isChannelArchived(targetChannel);
51+
const isTargetChannelPinned = isChannelPinned(targetChannel);
52+
const isArchivedFilterTrue = filters && filters.archived === true;
53+
const isArchivedFilterFalse = filters && filters.archived === false;
4854

49-
const considerArchivedChannels = filters && filters.archived === false;
55+
const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
5056

51-
// If channel is archived and we don't want to consider archived channels, return existing list
52-
if (isTargetChannelArchived && considerArchivedChannels) {
57+
if (
58+
// If the channel is archived and we are not considering archived channels
59+
(isTargetChannelArchived && isArchivedFilterFalse) ||
60+
// If the channel is pinned and we are not considering pinned channels
61+
(isTargetChannelPinned && considerPinnedChannels) ||
62+
lockChannelOrder
63+
) {
5364
return channels;
5465
}
5566

@@ -58,6 +69,14 @@ export const useNewMessage = <
5869
// It may happen that channel was hidden using channel.hide(). In that case
5970
// We remove it from `channels` state, but its still being watched and exists in client.activeChannels.
6071
const channel = client.channel(event.channel_type, event.channel_id);
72+
// While adding new channels, we need to consider whether they are archived or not.
73+
if (
74+
// When archived filter false, and channel is archived
75+
(isChannelArchived(channel) && isArchivedFilterFalse) ||
76+
// When archived filter true, and channel is not archived
77+
(isArchivedFilterTrue && !isChannelArchived(channel))
78+
)
79+
return channels;
6180
return [channel, ...channels];
6281
}
6382

package/src/components/ChannelList/hooks/utils/index.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Channel } from 'stream-chat';
22
import { DefaultStreamChatGenerics } from '../../../../types/types';
3+
import { ChannelListProps } from '../../ChannelList';
34

45
export const isChannelPinned = <
56
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
@@ -24,3 +25,62 @@ export const isChannelArchived = <
2425

2526
return !!member?.archived_at;
2627
};
28+
29+
/**
30+
* Returns true only if `{ pinned_at: -1 }` or `{ pinned_at: 1 }` option is first within the `sort` array.
31+
*/
32+
export const shouldConsiderPinnedChannels = <
33+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
34+
>(
35+
sort: ChannelListProps<StreamChatGenerics>['sort'],
36+
) => {
37+
if (!sort) return false;
38+
39+
if (Array.isArray(sort)) {
40+
const [option] = sort;
41+
42+
if (!option?.pinned_at) return false;
43+
44+
return Math.abs(option.pinned_at) === 1;
45+
} else {
46+
if (!sort.pinned_at) return false;
47+
48+
return Math.abs(sort.pinned_at) === 1;
49+
}
50+
};
51+
52+
export function findPinnedAtSortOrder<
53+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
54+
>({ sort }: { sort: ChannelListProps<StreamChatGenerics>['sort'] }) {
55+
if (!sort) return null;
56+
57+
if (Array.isArray(sort)) {
58+
const [option] = sort;
59+
60+
if (!option?.pinned_at) return null;
61+
62+
return option.pinned_at;
63+
} else {
64+
if (!sort.pinned_at) return null;
65+
66+
return sort.pinned_at;
67+
}
68+
}
69+
70+
export function findLastPinnedChannelIndex<
71+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
72+
>({ channels }: { channels: Channel<StreamChatGenerics>[] }) {
73+
let lastPinnedChannelIndex: number | null = null;
74+
75+
for (const channel of channels) {
76+
if (!isChannelPinned(channel)) break;
77+
78+
if (typeof lastPinnedChannelIndex === 'number') {
79+
lastPinnedChannelIndex++;
80+
} else {
81+
lastPinnedChannelIndex = 0;
82+
}
83+
}
84+
85+
return lastPinnedChannelIndex;
86+
}

0 commit comments

Comments
 (0)