Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
arnautov-anton committed Nov 7, 2024
1 parent 1a75c2a commit 7af0a7d
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 29 deletions.
14 changes: 10 additions & 4 deletions src/components/ChannelList/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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, moveChannelUp } from './utils';
import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils';

import { AvatarProps, Avatar as DefaultAvatar } from '../Avatar/Avatar';
import { ChannelPreview, ChannelPreviewUIComponentProps } from '../ChannelPreview/ChannelPreview';
Expand Down Expand Up @@ -62,6 +62,7 @@ export type ChannelListProps<
) => Array<Channel<StreamChatGenerics>>;
/** 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) */
ChannelSearch?: React.ComponentType<ChannelSearchProps<StreamChatGenerics>>;
// FIXME: how is this even legal (WHY IS IT STRING?!)
/** Set a channel (with this ID) to active and manually move it to the top of the list */
customActiveChannel?: string;
/** 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. */
Expand Down Expand Up @@ -228,6 +229,7 @@ const UnMemoizedChannelList = <
}

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

if (!customActiveChannelObject) {
Expand All @@ -238,10 +240,12 @@ const UnMemoizedChannelList = <
if (customActiveChannelObject) {
setActiveChannel(customActiveChannelObject, watchers);

const newChannels = moveChannelUp({
activeChannel: customActiveChannelObject,
const newChannels = moveChannelUpwards({
channels,
cid: customActiveChannelObject.cid,
channelToMove: customActiveChannelObject,
// TODO: adjust acordingly (based on sort)
considerPinnedChannels: false,
userId: client.userID!,
});

setChannels(newChannels);
Expand Down Expand Up @@ -298,6 +302,8 @@ const UnMemoizedChannelList = <
onMessageNewHandler,
lockChannelOrder,
allowNewMessagesFromUnfilteredChannels,
// TODO: adjust accordingly (consider sort option)
false,
);
useNotificationMessageNewListener(
setChannels,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChannelList/__tests__/ChannelList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
queryChannelsApi,
queryUsersApi,
useMockedApis,
} from 'mock-builders';
} from '../../../mock-builders';

import { Chat } from '../../Chat';
import { ChannelList } from '../ChannelList';
Expand Down
72 changes: 57 additions & 15 deletions src/components/ChannelList/hooks/useMessageNewListener.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,82 @@
import { useEffect } from 'react';
import uniqBy from 'lodash.uniqby';

import { moveChannelUp } from '../utils';
import type { Dispatch, SetStateAction } from 'react';

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

import type { Channel, Event } from 'stream-chat';
import type { Channel, Event, ExtendableGenerics } from 'stream-chat';

import type { DefaultStreamChatGenerics } from '../../../types/types';
import { moveChannelUpwards } from '../utils';

export const isChannelPinned = <SCG extends ExtendableGenerics>({
channel,
userId,
}: {
userId: string;
channel?: Channel<SCG>;
}) => {
if (!channel) return false;

const member = channel.state.members[userId];

return !!member?.pinned_at;
};

export const useMessageNewListener = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(
setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>,
setChannels: Dispatch<SetStateAction<Array<Channel<SCG>>>>,
customHandler?: (
setChannels: React.Dispatch<React.SetStateAction<Array<Channel<StreamChatGenerics>>>>,
event: Event<StreamChatGenerics>,
setChannels: Dispatch<SetStateAction<Array<Channel<SCG>>>>,
event: Event<SCG>,
) => void,
lockChannelOrder = false,
allowNewMessagesFromUnfilteredChannels = true,
considerPinnedChannels = false, // automatically set to true by checking sorting options (must include {pinned_at: -1/1})
) => {
const { client } = useChatContext<StreamChatGenerics>('useMessageNewListener');
const { client } = useChatContext<SCG>('useMessageNewListener');

useEffect(() => {
const handleEvent = (event: Event<StreamChatGenerics>) => {
const handleEvent = (event: Event<SCG>) => {
if (customHandler && typeof customHandler === 'function') {
customHandler(setChannels, event);
} else {
setChannels((channels) => {
const channelInList = channels.filter((channel) => channel.cid === event.cid).length > 0;
const targetChannelIndex = channels.findIndex((channel) => channel.cid === event.cid);
const targetChannelExistsWithinList = targetChannelIndex >= 0;

const isTargetChannelPinned = isChannelPinned({
channel: channels[targetChannelIndex],
userId: client.userID!,
});

if (!channelInList && allowNewMessagesFromUnfilteredChannels && event.channel_type) {
const channel = client.channel(event.channel_type, event.channel_id);
return uniqBy([channel, ...channels], 'cid');
if (
// target channel is pinned
(isTargetChannelPinned && considerPinnedChannels) ||
// list order is locked
lockChannelOrder ||
// target channel is not within the loaded list and loading from cache is disallowed
(!targetChannelExistsWithinList && !allowNewMessagesFromUnfilteredChannels)
) {
return channels;
}

if (!lockChannelOrder) return moveChannelUp({ channels, cid: event.cid || '' });
// we either have the channel to move or we pull it from the cache (or instantiate) if it's allowed
const channelToMove: Channel<SCG> | null =
channels[targetChannelIndex] ??
(allowNewMessagesFromUnfilteredChannels && event.channel_type
? client.channel(event.channel_type, event.channel_id)
: null);

if (channelToMove) {
return moveChannelUpwards({
channels,
channelToMove,
channelToMoveIndexWithinChannels: targetChannelIndex,
considerPinnedChannels,
userId: client.userID!,
});
}

return channels;
});
Expand All @@ -50,6 +91,7 @@ export const useMessageNewListener = <
}, [
allowNewMessagesFromUnfilteredChannels,
client,
considerPinnedChannels,
customHandler,
lockChannelOrder,
setChannels,
Expand Down
91 changes: 82 additions & 9 deletions src/components/ChannelList/utils.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,51 @@
import type { Channel } from 'stream-chat';
import uniqBy from 'lodash.uniqby';

import { isChannelPinned } from './hooks';

import type { DefaultStreamChatGenerics } from '../../types/types';

export const MAX_QUERY_CHANNELS_LIMIT = 30;

type MoveChannelUpParams<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = {
channels: Array<Channel<StreamChatGenerics>>;
type MoveChannelUpParams<SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
channels: Array<Channel<SCG>>;
cid: string;
activeChannel?: Channel<StreamChatGenerics>;
userId: string;
activeChannel?: Channel<SCG>;
channelIndexWithinChannels?: number;
considerPinnedChannels?: boolean;
};

export const moveChannelUp = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
type MoveChannelUpwardsParams<SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
channels: Array<Channel<SCG>>;
channelToMove: Channel<SCG>;
/**
* 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;
};

/**
* @deprecated
*/
export const moveChannelUp = <SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({
activeChannel,
channels,
cid,
}: MoveChannelUpParams<StreamChatGenerics>) => {
}: MoveChannelUpParams<SCG>) => {
// get index of channel to move up
const channelIndex = channels.findIndex((channel) => channel.cid === cid);

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

return uniqBy([channel, ...channels], 'cid');
};

export const moveChannelUpwards = <
SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
channels,
channelToMove,
channelToMoveIndexWithinChannels,
considerPinnedChannels = false,
userId,
}: MoveChannelUpwardsParams<SCG>) => {
// get index of channel to move up
const targetChannelIndex =
channelToMoveIndexWithinChannels ??
channels.findIndex((channel) => channel.cid === channelToMove.cid);

const targetChannelExistsWithinList = targetChannelIndex >= 0;
const targetChannelAlreadyAtTheTop = targetChannelIndex === 0;

if (targetChannelAlreadyAtTheTop) return channels;

// 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;
}
}
}

const newChannels = [...channels];

// target channel index is known, remove it from the list
if (targetChannelExistsWithinList) {
newChannels.splice(targetChannelIndex, 1);
}

// 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);

return newChannels;
};
2 changes: 2 additions & 0 deletions src/mock-builders/event/messageNew.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export default (client, newMessage, channel = {}) => {
client.dispatchEvent({
channel,
channel_id: channel.id,
channel_type: channel.type,
cid: channel.cid,
message: newMessage,
type: 'message.new',
Expand Down

0 comments on commit 7af0a7d

Please sign in to comment.