stream-chat-react 12.8.1 → 12.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ type HandleNotificationAddedToChannelParameters<SCG extends ExtendableGenerics>
24
24
  } & Required<Pick<ChannelListProps<SCG>, 'sort'>>;
25
25
  type HandleMemberUpdatedParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & {
26
26
  lockChannelOrder: boolean;
27
- } & Required<Pick<ChannelListProps<SCG>, 'sort'>>;
27
+ } & Required<Pick<ChannelListProps<SCG>, 'sort' | 'filters'>>;
28
28
  type HandleChannelDeletedParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & RepeatedParameters<SCG>;
29
29
  type HandleChannelHiddenParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & RepeatedParameters<SCG>;
30
30
  type HandleChannelVisibleParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & RepeatedParameters<SCG>;
@@ -37,9 +37,9 @@ export declare const useChannelListShapeDefaults: <SCG extends ExtendableGeneric
37
37
  handleChannelTruncated: ({ customHandler, event, setChannels }: HandleChannelTruncatedParameters<SCG>) => void;
38
38
  handleChannelUpdated: ({ customHandler, event, setChannels }: HandleChannelUpdatedParameters<SCG>) => void;
39
39
  handleChannelVisible: ({ customHandler, event, setChannels }: HandleChannelVisibleParameters<SCG>) => Promise<void>;
40
- handleMemberUpdated: ({ event, lockChannelOrder, setChannels, sort }: HandleMemberUpdatedParameters<SCG>) => void;
40
+ handleMemberUpdated: ({ event, filters, lockChannelOrder, setChannels, sort, }: HandleMemberUpdatedParameters<SCG>) => void;
41
41
  handleMessageNew: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, filters, lockChannelOrder, setChannels, sort, }: HandleMessageNewParameters<SCG>) => void;
42
- handleNotificationAddedToChannel: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, }: HandleNotificationAddedToChannelParameters<SCG>) => Promise<void>;
42
+ handleNotificationAddedToChannel: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, sort, }: HandleNotificationAddedToChannelParameters<SCG>) => Promise<void>;
43
43
  handleNotificationMessageNew: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, filters, setChannels, sort, }: HandleNotificationMessageNewParameters<SCG>) => Promise<void>;
44
44
  handleNotificationRemovedFromChannel: ({ customHandler, event, setChannels, }: HandleNotificationRemovedFromChannelParameters<SCG>) => void;
45
45
  handleUserPresenceChanged: ({ event, setChannels }: HandleUserPresenceChangedParameters<SCG>) => void;
@@ -1,7 +1,7 @@
1
1
  // const defaults = useChannelListShapeDefaults();
2
2
  import { useCallback, useEffect, useMemo, useRef } from 'react';
3
3
  import uniqBy from 'lodash.uniqby';
4
- import { findLastPinnedChannelIndex, isChannelArchived, isChannelPinned, moveChannelUpwards, shouldConsiderArchivedChannels, shouldConsiderPinnedChannels, } from '../utils';
4
+ import { extractSortValue, findLastPinnedChannelIndex, isChannelArchived, isChannelPinned, moveChannelUpwards, shouldConsiderArchivedChannels, shouldConsiderPinnedChannels, } from '../utils';
5
5
  import { useChatContext } from '../../../context';
6
6
  import { getChannel } from '../../../utils';
7
7
  const shared = ({ customHandler, event, setChannels, }) => {
@@ -22,39 +22,37 @@ export const useChannelListShapeDefaults = () => {
22
22
  if (typeof customHandler === 'function') {
23
23
  return customHandler(setChannels, event);
24
24
  }
25
- setChannels((channels) => {
26
- const targetChannelIndex = channels.findIndex((channel) => channel.cid === event.cid);
25
+ const channelType = event.channel_type;
26
+ const channelId = event.channel_id;
27
+ if (!channelType || !channelId)
28
+ return;
29
+ setChannels((currentChannels) => {
30
+ const targetChannel = client.channel(channelType, channelId);
31
+ const targetChannelIndex = currentChannels.indexOf(targetChannel);
27
32
  const targetChannelExistsWithinList = targetChannelIndex >= 0;
28
- const targetChannel = channels[targetChannelIndex];
29
33
  const isTargetChannelPinned = isChannelPinned(targetChannel);
30
34
  const isTargetChannelArchived = isChannelArchived(targetChannel);
31
35
  const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
32
36
  const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
33
37
  if (
34
- // target channel is archived
35
- (isTargetChannelArchived && considerArchivedChannels) ||
36
- // target channel is pinned
37
- (isTargetChannelPinned && considerPinnedChannels) ||
38
+ // filter is defined, target channel is archived and filter option is set to false
39
+ (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
40
+ // filter is defined, target channel isn't archived and filter option is set to true
41
+ (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
42
+ // sort option is defined, target channel is pinned
43
+ (considerPinnedChannels && isTargetChannelPinned) ||
38
44
  // list order is locked
39
45
  lockChannelOrder ||
40
46
  // target channel is not within the loaded list and loading from cache is disallowed
41
47
  (!targetChannelExistsWithinList && !allowNewMessagesFromUnfilteredChannels)) {
42
- return channels;
43
- }
44
- // we either have the channel to move or we pull it from the cache (or instantiate) if it's allowed
45
- const channelToMove = channels[targetChannelIndex] ??
46
- (allowNewMessagesFromUnfilteredChannels && event.channel_type
47
- ? client.channel(event.channel_type, event.channel_id)
48
- : null);
49
- if (channelToMove) {
50
- return moveChannelUpwards({
51
- channels,
52
- channelToMove,
53
- channelToMoveIndexWithinChannels: targetChannelIndex,
54
- sort,
55
- });
48
+ return currentChannels;
56
49
  }
57
- return channels;
50
+ return moveChannelUpwards({
51
+ channels: currentChannels,
52
+ channelToMove: targetChannel,
53
+ channelToMoveIndexWithinChannels: targetChannelIndex,
54
+ sort,
55
+ });
58
56
  });
59
57
  }, [client]);
60
58
  const handleNotificationMessageNew = useCallback(async ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, filters, setChannels, sort, }) => {
@@ -70,7 +68,7 @@ export const useChannelListShapeDefaults = () => {
70
68
  type: event.channel.type,
71
69
  });
72
70
  const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
73
- if (isChannelArchived(channel) && considerArchivedChannels) {
71
+ if (isChannelArchived(channel) && considerArchivedChannels && !filters.archived) {
74
72
  return;
75
73
  }
76
74
  if (!allowNewMessagesFromUnfilteredChannels) {
@@ -83,25 +81,31 @@ export const useChannelListShapeDefaults = () => {
83
81
  sort,
84
82
  }));
85
83
  }, [client]);
86
- const handleNotificationAddedToChannel = useCallback(async ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, }) => {
84
+ const handleNotificationAddedToChannel = useCallback(async ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, sort, }) => {
87
85
  if (typeof customHandler === 'function') {
88
86
  return customHandler(setChannels, event);
89
87
  }
90
- if (allowNewMessagesFromUnfilteredChannels && event.channel?.type) {
91
- const channel = await getChannel({
92
- client,
93
- id: event.channel.id,
94
- members: event.channel.members?.reduce((acc, { user, user_id }) => {
95
- const userId = user_id || user?.id;
96
- if (userId) {
97
- acc.push(userId);
98
- }
99
- return acc;
100
- }, []),
101
- type: event.channel.type,
102
- });
103
- setChannels((channels) => uniqBy([channel, ...channels], 'cid'));
88
+ if (!event.channel || !allowNewMessagesFromUnfilteredChannels) {
89
+ return;
104
90
  }
91
+ const channel = await getChannel({
92
+ client,
93
+ id: event.channel.id,
94
+ members: event.channel.members?.reduce((newMembers, { user, user_id }) => {
95
+ const userId = user_id || user?.id;
96
+ if (userId)
97
+ newMembers.push(userId);
98
+ return newMembers;
99
+ }, []),
100
+ type: event.channel.type,
101
+ });
102
+ // membership has been reset (target channel shouldn't be pinned nor archived)
103
+ setChannels((channels) => moveChannelUpwards({
104
+ channels,
105
+ channelToMove: channel,
106
+ channelToMoveIndexWithinChannels: -1,
107
+ sort,
108
+ }));
105
109
  }, [client]);
106
110
  const handleNotificationRemovedFromChannel = useCallback(({ customHandler, event, setChannels, }) => {
107
111
  if (typeof customHandler === 'function') {
@@ -109,21 +113,22 @@ export const useChannelListShapeDefaults = () => {
109
113
  }
110
114
  setChannels((channels) => channels.filter((channel) => channel.cid !== event.channel?.cid));
111
115
  }, []);
112
- const handleMemberUpdated = useCallback(({ event, lockChannelOrder, setChannels, sort }) => {
116
+ const handleMemberUpdated = useCallback(({ event, filters, lockChannelOrder, setChannels, sort, }) => {
113
117
  if (!event.member?.user || event.member.user.id !== client.userID || !event.channel_type) {
114
118
  return;
115
119
  }
116
- const member = event.member;
117
120
  const channelType = event.channel_type;
118
121
  const channelId = event.channel_id;
119
122
  const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
120
- // TODO: extract this and consider single property sort object too
121
- const pinnedAtSort = Array.isArray(sort) ? sort[0]?.pinned_at ?? null : null;
123
+ const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
124
+ const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });
122
125
  setChannels((currentChannels) => {
123
126
  const targetChannel = client.channel(channelType, channelId);
124
127
  // assumes that channel instances are not changing
125
128
  const targetChannelIndex = currentChannels.indexOf(targetChannel);
126
129
  const targetChannelExistsWithinList = targetChannelIndex >= 0;
130
+ const isTargetChannelArchived = isChannelArchived(targetChannel);
131
+ const isTargetChannelPinned = isChannelPinned(targetChannel);
127
132
  // handle pinning
128
133
  if (!considerPinnedChannels || lockChannelOrder)
129
134
  return currentChannels;
@@ -132,14 +137,15 @@ export const useChannelListShapeDefaults = () => {
132
137
  newChannels.splice(targetChannelIndex, 1);
133
138
  }
134
139
  // handle archiving (remove channel)
135
- if (typeof member.archived_at === 'string') {
140
+ if ((considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
141
+ (considerArchivedChannels && !isTargetChannelArchived && filters.archived)) {
136
142
  return newChannels;
137
143
  }
138
144
  let lastPinnedChannelIndex = null;
139
145
  // calculate last pinned channel index only if `pinned_at` sort is set to
140
146
  // ascending order or if it's in descending order while the pin is being removed, otherwise
141
147
  // we move to the top (index 0)
142
- if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !member.pinned_at)) {
148
+ if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !isTargetChannelPinned)) {
143
149
  lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
144
150
  }
145
151
  const newTargetChannelIndex = typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0;
@@ -311,6 +317,7 @@ export const usePrepareShapeHandlers = ({ allowNewMessagesFromUnfilteredChannels
311
317
  case 'member.updated':
312
318
  defaults.handleMemberUpdated({
313
319
  event,
320
+ filters,
314
321
  lockChannelOrder,
315
322
  setChannels,
316
323
  sort,
@@ -1,2 +1,3 @@
1
- import type { Channel, ExtendableGenerics } from 'stream-chat';
2
- export declare const useChannelMembershipState: <SCG extends ExtendableGenerics>(channel?: Channel<SCG>) => import("stream-chat").ChannelMemberResponse<SCG>;
1
+ import type { Channel, ChannelMemberResponse, ExtendableGenerics } from 'stream-chat';
2
+ export declare function useChannelMembershipState<SCG extends ExtendableGenerics>(channel: Channel<SCG>): ChannelMemberResponse<SCG>;
3
+ export declare function useChannelMembershipState<SCG extends ExtendableGenerics>(channel?: Channel<SCG> | undefined): ChannelMemberResponse<SCG> | undefined;
@@ -1,15 +1,6 @@
1
- import { useEffect, useState } from 'react';
2
- import { useChatContext } from '../../../context';
3
- export const useChannelMembershipState = (channel) => {
4
- const [membership, setMembership] = useState(channel?.state.membership || {});
5
- const { client } = useChatContext();
6
- useEffect(() => {
7
- if (!channel)
8
- return;
9
- const subscriptions = ['member.updated'].map((v) => client.on(v, () => {
10
- setMembership(channel.state.membership);
11
- }));
12
- return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
13
- }, [client, channel]);
14
- return membership;
15
- };
1
+ import { useSelectedChannelState } from './useSelectedChannelState';
2
+ const selector = (c) => c.state.membership;
3
+ const keys = ['member.updated'];
4
+ export function useChannelMembershipState(channel) {
5
+ return useSelectedChannelState({ channel, selector, stateChangeEventKeys: keys });
6
+ }
@@ -0,0 +1,11 @@
1
+ import type { Channel, EventTypes, ExtendableGenerics } from 'stream-chat';
2
+ export declare function useSelectedChannelState<SCG extends ExtendableGenerics, O>(_: {
3
+ channel: Channel<SCG>;
4
+ selector: (channel: Channel<SCG>) => O;
5
+ stateChangeEventKeys?: EventTypes[];
6
+ }): O;
7
+ export declare function useSelectedChannelState<SCG extends ExtendableGenerics, O>(_: {
8
+ selector: (channel: Channel<SCG>) => O;
9
+ channel?: Channel<SCG> | undefined;
10
+ stateChangeEventKeys?: EventTypes[];
11
+ }): O | undefined;
@@ -0,0 +1,20 @@
1
+ import { useCallback } from 'react';
2
+ import { useSyncExternalStore } from 'use-sync-external-store/shim';
3
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
4
+ const noop = () => { };
5
+ export function useSelectedChannelState({ channel, stateChangeEventKeys = ['all'], selector, }) {
6
+ const subscribe = useCallback((onStoreChange) => {
7
+ if (!channel)
8
+ return noop;
9
+ const subscriptions = stateChangeEventKeys.map((et) => channel.on(et, () => {
10
+ onStoreChange(selector(channel));
11
+ }));
12
+ return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
13
+ }, [channel, selector, stateChangeEventKeys]);
14
+ const getSnapshot = useCallback(() => {
15
+ if (!channel)
16
+ return undefined;
17
+ return selector(channel);
18
+ }, [channel, selector]);
19
+ return useSyncExternalStore(subscribe, getSnapshot);
20
+ }
@@ -1,4 +1,4 @@
1
- import type { Channel, ChannelSort, ExtendableGenerics } from 'stream-chat';
1
+ import type { Channel, ChannelSort, ChannelSortBase, ExtendableGenerics } from 'stream-chat';
2
2
  import type { DefaultStreamChatGenerics } from '../../types/types';
3
3
  import type { ChannelListProps } from './ChannelList';
4
4
  export declare const MAX_QUERY_CHANNELS_LIMIT = 30;
@@ -29,18 +29,28 @@ type MoveChannelUpwardsParams<SCG extends DefaultStreamChatGenerics = DefaultStr
29
29
  */
30
30
  channelToMoveIndexWithinChannels?: number;
31
31
  };
32
- /**
33
- * This function should not be used to move pinned already channels.
34
- */
35
32
  export declare const moveChannelUpwards: <SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ channels, channelToMove, channelToMoveIndexWithinChannels, sort, }: MoveChannelUpwardsParams<SCG>) => Channel<SCG>[];
36
33
  /**
37
- * Returns true only if `{ pinned_at: -1 }` or `{ pinned_at: 1 }` option is first within the `sort` array.
34
+ * Returns `true` only if object with `pinned_at` property is first within the `sort` array
35
+ * or if `pinned_at` key of the `sort` object gets picked first when using `for...in` looping mechanism
36
+ * and value of the `pinned_at` is either `1` or `-1`.
38
37
  */
39
38
  export declare const shouldConsiderPinnedChannels: <SCG extends ExtendableGenerics>(sort: ChannelListProps<SCG>['sort']) => boolean;
39
+ export declare const extractSortValue: <SCG extends ExtendableGenerics>({ atIndex, sort, targetKey, }: {
40
+ atIndex: number;
41
+ targetKey: keyof ChannelSortBase<SCG>;
42
+ sort?: ChannelListProps<SCG>['sort'];
43
+ }) => NonNullable<ChannelSortBase<SCG>["created_at" | "pinned_at" | "updated_at" | keyof SCG["channelType"] | "has_unread" | "last_message_at" | "last_updated" | "member_count" | "unread_count"]> | null;
40
44
  /**
41
- * Returns `true` only if `archived` property is set to `false` within `filters`.
45
+ * Returns `true` only if `archived` property is of type `boolean` within `filters` object.
42
46
  */
43
47
  export declare const shouldConsiderArchivedChannels: <SCG extends ExtendableGenerics>(filters: ChannelListProps<SCG>['filters']) => boolean;
48
+ /**
49
+ * Returns `true` only if `pinned_at` property is of type `string` within `membership` object.
50
+ */
44
51
  export declare const isChannelPinned: <SCG extends ExtendableGenerics>(channel: Channel<SCG>) => boolean;
52
+ /**
53
+ * Returns `true` only if `archived_at` property is of type `string` within `membership` object.
54
+ */
45
55
  export declare const isChannelArchived: <SCG extends ExtendableGenerics>(channel: Channel<SCG>) => boolean;
46
56
  export {};
@@ -31,9 +31,6 @@ export function findLastPinnedChannelIndex({ channels, }) {
31
31
  }
32
32
  return lastPinnedChannelIndex;
33
33
  }
34
- /**
35
- * This function should not be used to move pinned already channels.
36
- */
37
34
  export const moveChannelUpwards = ({ channels, channelToMove, channelToMoveIndexWithinChannels, sort, }) => {
38
35
  // get index of channel to move up
39
36
  const targetChannelIndex = channelToMoveIndexWithinChannels ??
@@ -44,8 +41,10 @@ export const moveChannelUpwards = ({ channels, channelToMove, channelToMoveIndex
44
41
  // receive messages and are not pinned should move upwards but only under the last pinned channel
45
42
  // in the list
46
43
  const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
47
- if (targetChannelAlreadyAtTheTop)
44
+ const isTargetChannelPinned = isChannelPinned(channelToMove);
45
+ if (targetChannelAlreadyAtTheTop || (considerPinnedChannels && isTargetChannelPinned)) {
48
46
  return channels;
47
+ }
49
48
  const newChannels = [...channels];
50
49
  // target channel index is known, remove it from the list
51
50
  if (targetChannelExistsWithinList) {
@@ -62,35 +61,62 @@ export const moveChannelUpwards = ({ channels, channelToMove, channelToMoveIndex
62
61
  return newChannels;
63
62
  };
64
63
  /**
65
- * Returns true only if `{ pinned_at: -1 }` or `{ pinned_at: 1 }` option is first within the `sort` array.
64
+ * Returns `true` only if object with `pinned_at` property is first within the `sort` array
65
+ * or if `pinned_at` key of the `sort` object gets picked first when using `for...in` looping mechanism
66
+ * and value of the `pinned_at` is either `1` or `-1`.
66
67
  */
67
68
  export const shouldConsiderPinnedChannels = (sort) => {
68
- if (!sort)
69
+ const value = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });
70
+ if (typeof value !== 'number')
69
71
  return false;
70
- if (!Array.isArray(sort))
71
- return false;
72
- const [option] = sort;
73
- if (!option?.pinned_at)
74
- return false;
75
- return Math.abs(option.pinned_at) === 1;
72
+ return Math.abs(value) === 1;
73
+ };
74
+ export const extractSortValue = ({ atIndex, sort, targetKey, }) => {
75
+ if (!sort)
76
+ return null;
77
+ let option = null;
78
+ if (Array.isArray(sort)) {
79
+ option = sort[atIndex] ?? null;
80
+ }
81
+ else {
82
+ let index = 0;
83
+ for (const key in sort) {
84
+ if (index !== atIndex) {
85
+ index++;
86
+ continue;
87
+ }
88
+ if (key !== targetKey) {
89
+ return null;
90
+ }
91
+ option = sort;
92
+ break;
93
+ }
94
+ }
95
+ return option?.[targetKey] ?? null;
76
96
  };
77
97
  /**
78
- * Returns `true` only if `archived` property is set to `false` within `filters`.
98
+ * Returns `true` only if `archived` property is of type `boolean` within `filters` object.
79
99
  */
80
100
  export const shouldConsiderArchivedChannels = (filters) => {
81
101
  if (!filters)
82
102
  return false;
83
- return !filters.archived;
103
+ return typeof filters.archived === 'boolean';
84
104
  };
105
+ /**
106
+ * Returns `true` only if `pinned_at` property is of type `string` within `membership` object.
107
+ */
85
108
  export const isChannelPinned = (channel) => {
86
109
  if (!channel)
87
110
  return false;
88
- const member = channel.state.membership;
89
- return !!member?.pinned_at;
111
+ const membership = channel.state.membership;
112
+ return typeof membership.pinned_at === 'string';
90
113
  };
114
+ /**
115
+ * Returns `true` only if `archived_at` property is of type `string` within `membership` object.
116
+ */
91
117
  export const isChannelArchived = (channel) => {
92
118
  if (!channel)
93
119
  return false;
94
- const member = channel.state.membership;
95
- return !!member?.archived_at;
120
+ const membership = channel.state.membership;
121
+ return typeof membership.archived_at === 'string';
96
122
  };
@@ -28,7 +28,7 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
28
28
  if (!userAgent.includes('stream-chat-react')) {
29
29
  // result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
30
30
  // the upper-case text between double underscores is replaced with the actual semantic version of the library
31
- client.setUserAgent(`stream-chat-react-12.8.1-${userAgent}`);
31
+ client.setUserAgent(`stream-chat-react-12.9.0-${userAgent}`);
32
32
  }
33
33
  client.threads.registerSubscriptions();
34
34
  client.polls.registerSubscriptions();
@@ -53,6 +53,8 @@ export type SuggestionListProps<StreamChatGenerics extends DefaultStreamChatGene
53
53
  };
54
54
  }>;
55
55
  export type ChatAutoCompleteProps<T extends UnknownType = UnknownType> = {
56
+ /** Override the default disabled state of the underlying `textarea` component. */
57
+ disabled?: boolean;
56
58
  /** Function to override the default submit handler on the underlying `textarea` component */
57
59
  handleSubmit?: (event: React.BaseSyntheticEvent) => void;
58
60
  /** Function to run on blur of the underlying `textarea` component */
@@ -28,6 +28,6 @@ const UnMemoizedChatAutoComplete = (props) => {
28
28
  innerRef.current = ref;
29
29
  }
30
30
  }, [innerRef]);
31
- return (React.createElement(AutoCompleteTextarea, { additionalTextareaProps: messageInput.additionalTextareaProps, "aria-label": cooldownRemaining ? t('Slow Mode ON') : placeholder, className: 'str-chat__textarea__textarea str-chat__message-textarea', closeCommandsList: messageInput.closeCommandsList, closeMentionsList: messageInput.closeMentionsList, containerClassName: 'str-chat__textarea str-chat__message-textarea-react-host', disabled: disabled || !!cooldownRemaining, disableMentions: messageInput.disableMentions, grow: messageInput.grow, handleSubmit: props.handleSubmit || messageInput.handleSubmit, innerRef: updateInnerRef, loadingComponent: LoadingIndicator, maxRows: messageInput.maxRows, minChar: 0, minRows: messageInput.minRows, onBlur: props.onBlur, onChange: props.onChange || messageInput.handleChange, onFocus: props.onFocus, onPaste: props.onPaste || messageInput.onPaste, placeholder: cooldownRemaining ? t('Slow Mode ON') : placeholder, replaceWord: emojiReplace, rows: props.rows || 1, shouldSubmit: messageInput.shouldSubmit, showCommandsList: messageInput.showCommandsList, showMentionsList: messageInput.showMentionsList, SuggestionItem: SuggestionItem, SuggestionList: SuggestionList, trigger: messageInput.autocompleteTriggers || {}, value: props.value || messageInput.text }));
31
+ return (React.createElement(AutoCompleteTextarea, { additionalTextareaProps: messageInput.additionalTextareaProps, "aria-label": cooldownRemaining ? t('Slow Mode ON') : placeholder, className: 'str-chat__textarea__textarea str-chat__message-textarea', closeCommandsList: messageInput.closeCommandsList, closeMentionsList: messageInput.closeMentionsList, containerClassName: 'str-chat__textarea str-chat__message-textarea-react-host', disabled: (props.disabled ?? disabled) || !!cooldownRemaining, disableMentions: messageInput.disableMentions, grow: messageInput.grow, handleSubmit: props.handleSubmit || messageInput.handleSubmit, innerRef: updateInnerRef, loadingComponent: LoadingIndicator, maxRows: messageInput.maxRows, minChar: 0, minRows: messageInput.minRows, onBlur: props.onBlur, onChange: props.onChange || messageInput.handleChange, onFocus: props.onFocus, onPaste: props.onPaste || messageInput.onPaste, placeholder: cooldownRemaining ? t('Slow Mode ON') : placeholder, replaceWord: emojiReplace, rows: props.rows || 1, shouldSubmit: messageInput.shouldSubmit, showCommandsList: messageInput.showCommandsList, showMentionsList: messageInput.showMentionsList, SuggestionItem: SuggestionItem, SuggestionList: SuggestionList, trigger: messageInput.autocompleteTriggers || {}, value: props.value || messageInput.text }));
32
32
  };
33
33
  export const ChatAutoComplete = React.memo(UnMemoizedChatAutoComplete);
@@ -5,7 +5,7 @@ import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyState
5
5
  import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
6
6
  import { isMessageEdited, Message } from '../Message';
7
7
  import { useComponentContext } from '../../context';
8
- import { isDateSeparatorMessage } from './utils';
8
+ import { getIsFirstUnreadMessage, isDateSeparatorMessage } from './utils';
9
9
  const PREPEND_OFFSET = 10 ** 7;
10
10
  export function calculateItemIndex(virtuosoIndex, numItemsPrepended) {
11
11
  return virtuosoIndex + numItemsPrepended - PREPEND_OFFSET;
@@ -72,24 +72,17 @@ export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
72
72
  (maybePrevMessage && isMessageEdited(maybePrevMessage)));
73
73
  const endOfGroup = shouldGroupByUser &&
74
74
  (message.user?.id !== maybeNextMessage?.user?.id || isMessageEdited(message));
75
- const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
76
- const lastReadTimestamp = lastReadDate?.getTime();
77
- const isFirstMessage = streamMessageIndex === 0;
78
- const isNewestMessage = lastReadMessageId === lastReceivedMessageId;
79
- const isLastReadMessage = message.id === lastReadMessageId ||
80
- (!unreadMessageCount && createdAtTimestamp === lastReadTimestamp);
81
- const isFirstUnreadMessage = firstUnreadMessageId === message.id ||
82
- (!!unreadMessageCount &&
83
- createdAtTimestamp &&
84
- lastReadTimestamp &&
85
- createdAtTimestamp > lastReadTimestamp &&
86
- isFirstMessage);
87
- const showUnreadSeparatorAbove = !lastReadMessageId && isFirstUnreadMessage;
88
- const showUnreadSeparatorBelow = isLastReadMessage && !isNewestMessage && (firstUnreadMessageId || !!unreadMessageCount);
75
+ const isFirstUnreadMessage = getIsFirstUnreadMessage({
76
+ firstUnreadMessageId,
77
+ isFirstMessage: streamMessageIndex === 0,
78
+ lastReadDate,
79
+ lastReadMessageId,
80
+ message,
81
+ previousMessage: streamMessageIndex ? messageList[streamMessageIndex - 1] : undefined,
82
+ unreadMessageCount,
83
+ });
89
84
  return (React.createElement(React.Fragment, null,
90
- showUnreadSeparatorAbove && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
85
+ isFirstUnreadMessage && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
91
86
  React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount }))),
92
- React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList }),
93
- showUnreadSeparatorBelow && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
94
- React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount })))));
87
+ React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList })));
95
88
  };
@@ -26,7 +26,6 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
26
26
  markRead();
27
27
  };
28
28
  const handleMessageNew = (event) => {
29
- const newMessageToCurrentChannel = event.cid === channel.cid;
30
29
  const isOwnMessage = event.user?.id && event.user.id === client.user?.id;
31
30
  const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
32
31
  if (isOwnMessage)
@@ -45,13 +44,11 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
45
44
  };
46
45
  });
47
46
  }
48
- else if (newMessageToCurrentChannel &&
49
- mainChannelUpdated &&
50
- shouldMarkRead(channel.countUnread())) {
47
+ else if (mainChannelUpdated && shouldMarkRead(channel.countUnread())) {
51
48
  markRead();
52
49
  }
53
50
  };
54
- client.on('message.new', handleMessageNew);
51
+ channel.on('message.new', handleMessageNew);
55
52
  document.addEventListener('visibilitychange', onVisibilityChange);
56
53
  const hasScrolledToBottom = previousRenderMessageListScrolledToBottom.current !== isMessageListScrolledToBottom &&
57
54
  isMessageListScrolledToBottom;
@@ -59,7 +56,7 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
59
56
  markRead();
60
57
  previousRenderMessageListScrolledToBottom.current = isMessageListScrolledToBottom;
61
58
  return () => {
62
- client.off('message.new', handleMessageNew);
59
+ channel.off('message.new', handleMessageNew);
63
60
  document.removeEventListener('visibilitychange', onVisibilityChange);
64
61
  };
65
62
  }, [
@@ -1,5 +1,5 @@
1
1
  import React, { Fragment } from 'react';
2
- import { isDateSeparatorMessage } from './utils';
2
+ import { getIsFirstUnreadMessage, isDateSeparatorMessage } from './utils';
3
3
  import { Message } from '../Message';
4
4
  import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
5
5
  import { EventComponent as DefaultMessageSystem } from '../EventComponent';
@@ -9,6 +9,7 @@ export function defaultRenderMessages({ channelUnreadUiState, components, custom
9
9
  const { DateSeparator = DefaultDateSeparator, HeaderComponent, MessageSystem = DefaultMessageSystem, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, } = components;
10
10
  const renderedMessages = [];
11
11
  let firstMessage;
12
+ let previousMessage = undefined;
12
13
  for (let index = 0; index < messages.length; index++) {
13
14
  const message = messages[index];
14
15
  if (isDateSeparatorMessage(message)) {
@@ -29,29 +30,21 @@ export function defaultRenderMessages({ channelUnreadUiState, components, custom
29
30
  }
30
31
  const groupStyles = messageGroupStyles[message.id] || '';
31
32
  const messageClass = customClasses?.message || `str-chat__li str-chat__li--${groupStyles}`;
32
- const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
33
- const lastReadTimestamp = channelUnreadUiState?.last_read.getTime();
34
- const isFirstMessage = firstMessage?.id && firstMessage.id === message.id;
35
- const isNewestMessage = index === messages.length - 1;
36
- const isLastReadMessage = channelUnreadUiState?.last_read_message_id === message.id ||
37
- (!channelUnreadUiState?.unread_messages && createdAtTimestamp === lastReadTimestamp);
38
- const isFirstUnreadMessage = channelUnreadUiState?.first_unread_message_id === message.id ||
39
- (!!channelUnreadUiState?.unread_messages &&
40
- !!createdAtTimestamp &&
41
- !!lastReadTimestamp &&
42
- createdAtTimestamp > lastReadTimestamp &&
43
- isFirstMessage);
44
- const showUnreadSeparatorAbove = !channelUnreadUiState?.last_read_message_id && isFirstUnreadMessage;
45
- const showUnreadSeparatorBelow = isLastReadMessage &&
46
- !isNewestMessage &&
47
- (channelUnreadUiState?.first_unread_message_id || !!channelUnreadUiState?.unread_messages); // this part has to be here as we do not mark channel read when sending a message
33
+ const isFirstUnreadMessage = getIsFirstUnreadMessage({
34
+ firstUnreadMessageId: channelUnreadUiState?.first_unread_message_id,
35
+ isFirstMessage: !!firstMessage?.id && firstMessage.id === message.id,
36
+ lastReadDate: channelUnreadUiState?.last_read,
37
+ lastReadMessageId: channelUnreadUiState?.last_read_message_id,
38
+ message,
39
+ previousMessage,
40
+ unreadMessageCount: channelUnreadUiState?.unread_messages,
41
+ });
48
42
  renderedMessages.push(React.createElement(Fragment, { key: message.id || message.created_at },
49
- showUnreadSeparatorAbove && UnreadMessagesSeparator && (React.createElement("li", { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
43
+ isFirstUnreadMessage && UnreadMessagesSeparator && (React.createElement("li", { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
50
44
  React.createElement(UnreadMessagesSeparator, { unreadCount: channelUnreadUiState?.unread_messages }))),
51
45
  React.createElement("li", { className: messageClass, "data-message-id": message.id, "data-testid": messageClass },
52
- React.createElement(Message, { groupStyles: [groupStyles], lastReceivedId: lastReceivedId, message: message, readBy: readData[message.id] || [], ...messageProps })),
53
- showUnreadSeparatorBelow && UnreadMessagesSeparator && (React.createElement("li", { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
54
- React.createElement(UnreadMessagesSeparator, { unreadCount: channelUnreadUiState?.unread_messages })))));
46
+ React.createElement(Message, { groupStyles: [groupStyles], lastReceivedId: lastReceivedId, message: message, readBy: readData[message.id] || [], ...messageProps }))));
47
+ previousMessage = message;
55
48
  }
56
49
  }
57
50
  return renderedMessages;
@@ -68,4 +68,13 @@ type DateSeparatorMessage = {
68
68
  unread: boolean;
69
69
  };
70
70
  export declare function isDateSeparatorMessage<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(message: StreamMessage<StreamChatGenerics>): message is DateSeparatorMessage;
71
+ export declare const getIsFirstUnreadMessage: ({ firstUnreadMessageId, isFirstMessage, lastReadDate, lastReadMessageId, message, previousMessage, unreadMessageCount, }: {
72
+ isFirstMessage: boolean;
73
+ message: StreamMessage;
74
+ firstUnreadMessageId?: string;
75
+ lastReadDate?: Date;
76
+ lastReadMessageId?: string;
77
+ previousMessage?: StreamMessage;
78
+ unreadMessageCount?: number;
79
+ }) => boolean;
71
80
  export {};
@@ -241,3 +241,11 @@ export const hasNotMoreMessages = (returnedCountMessages, limit) => returnedCoun
241
241
  export function isDateSeparatorMessage(message) {
242
242
  return message.customType === CUSTOM_MESSAGE_TYPE.date && !!message.date && isDate(message.date);
243
243
  }
244
+ export const getIsFirstUnreadMessage = ({ firstUnreadMessageId, isFirstMessage, lastReadDate, lastReadMessageId, message, previousMessage, unreadMessageCount = 0, }) => {
245
+ const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
246
+ const lastReadTimestamp = lastReadDate?.getTime();
247
+ const messageIsUnread = !!createdAtTimestamp && !!lastReadTimestamp && createdAtTimestamp > lastReadTimestamp;
248
+ const previousMessageIsLastRead = !!lastReadMessageId && lastReadMessageId === previousMessage?.id;
249
+ return (firstUnreadMessageId === message.id ||
250
+ (!!unreadMessageCount && messageIsUnread && (isFirstMessage || previousMessageIsLastRead)));
251
+ };