diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx index 32a5eab85..9f3c16f0c 100644 --- a/src/components/ChannelList/ChannelList.tsx +++ b/src/components/ChannelList/ChannelList.tsx @@ -15,7 +15,12 @@ import { useNotificationMessageNewListener } from './hooks/useNotificationMessag import { useNotificationRemovedFromChannelListener } from './hooks/useNotificationRemovedFromChannelListener'; import { CustomQueryChannelsFn, usePaginatedChannels } from './hooks/usePaginatedChannels'; import { useUserPresenceChangedListener } from './hooks/useUserPresenceChangedListener'; -import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils'; +import { useMemberUpdatedListener } from './hooks/useMemberUpdatedListener'; +import { + MAX_QUERY_CHANNELS_LIMIT, + moveChannelUpwards, + shouldConsiderPinnedChannels, +} from './utils'; import { AvatarProps, Avatar as DefaultAvatar } from '../Avatar/Avatar'; import { ChannelPreview, ChannelPreviewUIComponentProps } from '../ChannelPreview/ChannelPreview'; @@ -244,8 +249,7 @@ const UnMemoizedChannelList = < channels, channelToMove: customActiveChannelObject, // TODO: adjust acordingly (based on sort) - considerPinnedChannels: false, - userId: client.userID!, + considerPinnedChannels: shouldConsiderPinnedChannels(sort), }); setChannels(newChannels); @@ -295,6 +299,8 @@ const UnMemoizedChannelList = < const loadedChannels = channelRenderFilterFn ? channelRenderFilterFn(channels) : channels; + const considerPinnedChannels = shouldConsiderPinnedChannels(sort); + useMobileNavigation(channelListRef, navOpen, closeMobileNav); useMessageNewListener( @@ -303,7 +309,7 @@ const UnMemoizedChannelList = < lockChannelOrder, allowNewMessagesFromUnfilteredChannels, // TODO: adjust accordingly (consider sort option) - false, + considerPinnedChannels, ); useNotificationMessageNewListener( setChannels, @@ -315,6 +321,10 @@ const UnMemoizedChannelList = < onAddedToChannel, allowNewMessagesFromUnfilteredChannels, ); + useMemberUpdatedListener({ + considerPinnedChannels, + setChannels, + }); useNotificationRemovedFromChannelListener(setChannels, onRemovedFromChannel); useChannelDeletedListener(setChannels, onChannelDeleted); useChannelHiddenListener(setChannels, onChannelHidden); diff --git a/src/components/ChannelList/hooks/index.ts b/src/components/ChannelList/hooks/index.ts index 299b59565..3e7a896ce 100644 --- a/src/components/ChannelList/hooks/index.ts +++ b/src/components/ChannelList/hooks/index.ts @@ -11,3 +11,4 @@ export * from './useNotificationMessageNewListener'; export * from './useNotificationRemovedFromChannelListener'; export * from './usePaginatedChannels'; export * from './useUserPresenceChangedListener'; +export * from './useChannelMembershipState'; diff --git a/src/components/ChannelList/hooks/useChannelMembershipState.ts b/src/components/ChannelList/hooks/useChannelMembershipState.ts new file mode 100644 index 000000000..faf48d1b2 --- /dev/null +++ b/src/components/ChannelList/hooks/useChannelMembershipState.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; +import type { Channel, ChannelState, ExtendableGenerics } from 'stream-chat'; + +import { useChatContext } from '../../../context'; + +export const useChannelMembershipState = ( + channel?: Channel, +) => { + const [membership, setMembership] = useState['membership']>( + channel?.state.membership || {}, + ); + + const { client } = useChatContext(); + + useEffect(() => { + if (!channel) return; + + const subscriptions = ['member.updated'].map((v) => + client.on(v, () => { + setMembership(channel.state.membership); + }), + ); + + return () => subscriptions.forEach((subscription) => subscription.unsubscribe()); + }, [client, channel]); + + return membership; +}; diff --git a/src/components/ChannelList/hooks/useMemberUpdatedListener.ts b/src/components/ChannelList/hooks/useMemberUpdatedListener.ts new file mode 100644 index 000000000..83a4a1f0d --- /dev/null +++ b/src/components/ChannelList/hooks/useMemberUpdatedListener.ts @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import type { Channel, ExtendableGenerics } from 'stream-chat'; + +import { useChatContext } from '../../../context'; +import { findLastPinnedChannelIndex } from '../utils'; + +export const useMemberUpdatedListener = ({ + considerPinnedChannels = false, + lockChannelOrder = false, + setChannels, +}: { + setChannels: Dispatch[]>>; + considerPinnedChannels?: boolean; + lockChannelOrder?: boolean; +}) => { + const { client } = useChatContext(); + + useEffect(() => { + // do nothing if channel order is locked or pinned channels aren't being considered + if (lockChannelOrder || !considerPinnedChannels) return; + + const subscription = client.on('member.updated', (e) => { + if (!e.member || !e.channel_type) return; + // const member = e.member; + const channelType = e.channel_type; + const channelId = e.channel_id; + + setChannels((currentChannels) => { + const targetChannel = client.channel(channelType, channelId); + // assumes that channel instances are not changing + const targetChannelIndex = currentChannels.indexOf(targetChannel); + const targetChannelExistsWithinList = targetChannelIndex >= 0; + + const newChannels = [...currentChannels]; + + if (targetChannelExistsWithinList) { + newChannels.splice(targetChannelIndex, 1); + } + + const lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels }) ?? 0; + const newTargetChannelIndex = lastPinnedChannelIndex + 1; + + // skip re-render if the position of the channel does not change + if (currentChannels[newTargetChannelIndex] === targetChannel) { + return currentChannels; + } + + newChannels.splice(newTargetChannelIndex, 0, targetChannel); + + return newChannels; + }); + }); + + return subscription.unsubscribe; + }, [client, considerPinnedChannels, lockChannelOrder, setChannels]); +}; diff --git a/src/components/ChannelList/hooks/useMessageNewListener.ts b/src/components/ChannelList/hooks/useMessageNewListener.ts index 0060748bc..5d5d65c68 100644 --- a/src/components/ChannelList/hooks/useMessageNewListener.ts +++ b/src/components/ChannelList/hooks/useMessageNewListener.ts @@ -1,23 +1,19 @@ import { useEffect } from 'react'; import type { Dispatch, SetStateAction } from 'react'; - -import { useChatContext } from '../../../context/ChatContext'; - import type { Channel, Event, ExtendableGenerics } from 'stream-chat'; -import type { DefaultStreamChatGenerics } from '../../../types/types'; import { moveChannelUpwards } from '../utils'; +import { useChatContext } from '../../../context/ChatContext'; +import type { DefaultStreamChatGenerics } from '../../../types/types'; export const isChannelPinned = ({ channel, - userId, }: { - userId: string; channel?: Channel; }) => { if (!channel) return false; - const member = channel.state.members[userId]; + const member = channel.state.membership; return !!member?.pinned_at; }; @@ -47,7 +43,6 @@ export const useMessageNewListener = < const isTargetChannelPinned = isChannelPinned({ channel: channels[targetChannelIndex], - userId: client.userID!, }); if ( @@ -74,7 +69,6 @@ export const useMessageNewListener = < channelToMove, channelToMoveIndexWithinChannels: targetChannelIndex, considerPinnedChannels, - userId: client.userID!, }); } diff --git a/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts b/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts index 6fd242b7d..b3b90bcf3 100644 --- a/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts +++ b/src/components/ChannelList/hooks/useNotificationMessageNewListener.ts @@ -9,6 +9,7 @@ import type { Channel, Event } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../../types/types'; +// TODO: re-visit this and adjust (apply pinned channels) export const useNotificationMessageNewListener = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics >( diff --git a/src/components/ChannelList/utils.ts b/src/components/ChannelList/utils.ts index 668ac8cff..e89213470 100644 --- a/src/components/ChannelList/utils.ts +++ b/src/components/ChannelList/utils.ts @@ -1,41 +1,16 @@ -import type { Channel } from 'stream-chat'; import uniqBy from 'lodash.uniqby'; +import type { Channel, ExtendableGenerics } from 'stream-chat'; import { isChannelPinned } from './hooks'; - import type { DefaultStreamChatGenerics } from '../../types/types'; +import { ChannelListProps } from './ChannelList'; export const MAX_QUERY_CHANNELS_LIMIT = 30; type MoveChannelUpParams = { channels: Array>; cid: string; - userId: string; activeChannel?: Channel; - channelIndexWithinChannels?: number; - considerPinnedChannels?: boolean; -}; - -type MoveChannelUpwardsParams = { - channels: Array>; - channelToMove: Channel; - /** - * If the index of the channel within `channels` list which is being moved upwards - * (`channelToMove`) is known, you can supply it to skip extra calculation. - */ - channelToMoveIndexWithinChannels?: number; - /** - * Pinned channels should not move within the list based on recent activity, channels which - * receive messages and are not pinned should move upwards but only under the last pinned channel - * in the list. Property defaults to `false` and should be calculated based on existence of - * the `pinned_at` sort option. - */ - considerPinnedChannels?: boolean; - /** - * If `considerPinnedChannels` is set to `true`, then `userId` should be supplied - without it the - * pinned channels won't be considered. - */ - userId?: string; }; /** @@ -57,6 +32,48 @@ export const moveChannelUp = ({ + channels, +}: { + channels: Channel[]; +}) { + let lastPinnedChannelIndex: number | null = null; + + for (const channel of channels) { + if (!isChannelPinned({ channel })) break; + + if (typeof lastPinnedChannelIndex === 'number') { + lastPinnedChannelIndex++; + } else { + lastPinnedChannelIndex = 0; + } + } + + return lastPinnedChannelIndex; +} + +type MoveChannelUpwardsParams = { + channels: Array>; + channelToMove: Channel; + /** + * If the index of the channel within `channels` list which is being moved upwards + * (`channelToMove`) is known, you can supply it to skip extra calculation. + */ + channelToMoveIndexWithinChannels?: number; + /** + * Pinned channels should not move within the list based on recent activity, channels which + * receive messages and are not pinned should move upwards but only under the last pinned channel + * in the list. Property defaults to `false` and should be calculated based on existence of + * the `pinned_at` sort option. + */ + considerPinnedChannels?: boolean; +}; + export const moveChannelUpwards = < SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics >({ @@ -64,7 +81,6 @@ export const moveChannelUpwards = < channelToMove, channelToMoveIndexWithinChannels, considerPinnedChannels = false, - userId, }: MoveChannelUpwardsParams) => { // get index of channel to move up const targetChannelIndex = @@ -78,17 +94,9 @@ export const moveChannelUpwards = < // as position of pinned channels has to stay unchanged, we need to // find last pinned channel in the list to move the target channel after - let lastPinIndex: number | null = null; - if (considerPinnedChannels && userId) { - for (const c of channels) { - if (!isChannelPinned({ channel: c, userId })) break; - - if (typeof lastPinIndex === 'number') { - lastPinIndex++; - } else { - lastPinIndex = 0; - } - } + let lastPinnedChannelIndex: number | null = null; + if (considerPinnedChannels) { + lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels }); } const newChannels = [...channels]; @@ -99,7 +107,22 @@ export const moveChannelUpwards = < } // re-insert it at the new place (to specific index if pinned channels are considered) - newChannels.splice(typeof lastPinIndex === 'number' ? lastPinIndex + 1 : 0, 0, channelToMove); + newChannels.splice( + typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0, + 0, + channelToMove, + ); return newChannels; }; + +// TODO: adjust and re-test when the actual behavior is implemented by the BE +export const shouldConsiderPinnedChannels = (sort: ChannelListProps['sort']) => { + if (!sort) return false; + + if (Array.isArray(sort)) { + return sort.some((v) => v.pinned_at === -1); + } + + return sort.pinned_at === -1; +};