stream-chat-react 12.8.0 → 12.8.2

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.
@@ -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.0-${userAgent}`);
31
+ client.setUserAgent(`stream-chat-react-12.8.2-${userAgent}`);
32
32
  }
33
33
  client.threads.registerSubscriptions();
34
34
  client.polls.registerSubscriptions();
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { StateStore } from 'stream-chat';
2
3
  export type GetOrCreateDialogParams = {
3
4
  id: DialogId;
@@ -8,6 +9,7 @@ export type Dialog = {
8
9
  id: DialogId;
9
10
  isOpen: boolean | undefined;
10
11
  open: (zIndex?: number) => void;
12
+ removalTimeout: NodeJS.Timeout | undefined;
11
13
  remove: () => void;
12
14
  toggle: (closeAll?: boolean) => void;
13
15
  };
@@ -39,5 +41,11 @@ export declare class DialogManager {
39
41
  closeAll(): void;
40
42
  toggle(params: GetOrCreateDialogParams, closeAll?: boolean): void;
41
43
  remove(id: DialogId): void;
44
+ /**
45
+ * Marks the dialog state as unused. If the dialog id is referenced again quickly,
46
+ * the state will not be removed. Otherwise, the state will be removed after
47
+ * a short timeout.
48
+ */
49
+ markForRemoval(id: DialogId): void;
42
50
  }
43
51
  export {};
@@ -35,6 +35,7 @@ export class DialogManager {
35
35
  open: () => {
36
36
  this.open({ id });
37
37
  },
38
+ removalTimeout: undefined,
38
39
  remove: () => {
39
40
  this.remove(id);
40
41
  },
@@ -47,6 +48,21 @@ export class DialogManager {
47
48
  ...{ dialogsById: { ...current.dialogsById, [id]: dialog } },
48
49
  }));
49
50
  }
51
+ if (dialog.removalTimeout) {
52
+ clearTimeout(dialog.removalTimeout);
53
+ this.state.next((current) => ({
54
+ ...current,
55
+ ...{
56
+ dialogsById: {
57
+ ...current.dialogsById,
58
+ [id]: {
59
+ ...dialog,
60
+ removalTimeout: undefined,
61
+ },
62
+ },
63
+ },
64
+ }));
65
+ }
50
66
  return dialog;
51
67
  }
52
68
  open(params, closeRest) {
@@ -86,6 +102,9 @@ export class DialogManager {
86
102
  const dialog = state.dialogsById[id];
87
103
  if (!dialog)
88
104
  return;
105
+ if (dialog.removalTimeout) {
106
+ clearTimeout(dialog.removalTimeout);
107
+ }
89
108
  this.state.next((current) => {
90
109
  const newDialogs = { ...current.dialogsById };
91
110
  delete newDialogs[id];
@@ -95,4 +114,27 @@ export class DialogManager {
95
114
  };
96
115
  });
97
116
  }
117
+ /**
118
+ * Marks the dialog state as unused. If the dialog id is referenced again quickly,
119
+ * the state will not be removed. Otherwise, the state will be removed after
120
+ * a short timeout.
121
+ */
122
+ markForRemoval(id) {
123
+ const dialog = this.state.getLatestValue().dialogsById[id];
124
+ if (!dialog) {
125
+ return;
126
+ }
127
+ this.state.next((current) => ({
128
+ ...current,
129
+ dialogsById: {
130
+ ...current.dialogsById,
131
+ [id]: {
132
+ ...dialog,
133
+ removalTimeout: setTimeout(() => {
134
+ this.remove(id);
135
+ }, 16),
136
+ },
137
+ },
138
+ }));
139
+ }
98
140
  }
@@ -4,7 +4,11 @@ import { useStateStore } from '../../../store';
4
4
  export const useDialog = ({ id }) => {
5
5
  const { dialogManager } = useDialogManager();
6
6
  useEffect(() => () => {
7
- dialogManager.remove(id);
7
+ // Since this cleanup can run even if the component is still mounted
8
+ // and dialog id is unchanged (e.g. in <StrictMode />), it's safer to
9
+ // mark state as unused and only remove it after a timeout, rather than
10
+ // to remove it immediately.
11
+ dialogManager.markForRemoval(id);
8
12
  }, [dialogManager, id]);
9
13
  return dialogManager.getOrCreate({ id });
10
14
  };
@@ -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
+ };
@@ -122,7 +122,7 @@ var useDialog = ({ id }) => {
122
122
  const { dialogManager } = useDialogManager();
123
123
  (0, import_react6.useEffect)(
124
124
  () => () => {
125
- dialogManager.remove(id);
125
+ dialogManager.markForRemoval(id);
126
126
  },
127
127
  [dialogManager, id]
128
128
  );