stream-chat-react 12.9.0 → 12.10.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.
Files changed (43) hide show
  1. package/dist/components/Attachment/components/WaveProgressBar.js +1 -1
  2. package/dist/components/Attachment/hooks/useAudioController.d.ts +1 -1
  3. package/dist/components/Attachment/hooks/useAudioController.js +1 -1
  4. package/dist/components/AutoCompleteTextarea/Textarea.js +0 -4
  5. package/dist/components/AutoCompleteTextarea/utils.js +1 -5
  6. package/dist/components/Channel/Channel.js +3 -2
  7. package/dist/components/Channel/channelState.d.ts +1 -3
  8. package/dist/components/Channel/channelState.js +1 -1
  9. package/dist/components/Channel/hooks/useIsMounted.d.ts +1 -1
  10. package/dist/components/ChannelList/hooks/useChannelListShape.js +2 -2
  11. package/dist/components/ChannelList/hooks/useMobileNavigation.d.ts +1 -1
  12. package/dist/components/ChannelList/hooks/usePaginatedChannels.js +1 -1
  13. package/dist/components/ChannelSearch/SearchBar.d.ts +1 -1
  14. package/dist/components/ChannelSearch/SearchInput.d.ts +1 -1
  15. package/dist/components/ChannelSearch/hooks/useChannelSearch.js +1 -1
  16. package/dist/components/Chat/hooks/useChat.js +1 -1
  17. package/dist/components/Gallery/BaseImage.d.ts +1 -3
  18. package/dist/components/Gallery/Gallery.js +10 -2
  19. package/dist/components/Gallery/ModalGallery.js +5 -4
  20. package/dist/components/InfiniteScrollPaginator/InfiniteScroll.js +4 -4
  21. package/dist/components/MessageActions/hooks/useMessageActionsBoxPopper.d.ts +1 -1
  22. package/dist/components/MessageInput/hooks/useMessageInputText.d.ts +1 -1
  23. package/dist/components/MessageInput/hooks/useMessageInputText.js +2 -2
  24. package/dist/components/MessageInput/hooks/useTimeElapsed.js +1 -1
  25. package/dist/components/MessageList/MessageList.js +0 -1
  26. package/dist/components/MessageList/VirtualizedMessageList.d.ts +1 -1
  27. package/dist/components/MessageList/VirtualizedMessageList.js +2 -2
  28. package/dist/components/MessageList/hooks/MessageList/useMessageListScrollManager.js +1 -1
  29. package/dist/components/MessageList/hooks/VirtualizedMessageList/useMessageSetKey.js +1 -1
  30. package/dist/components/MessageList/hooks/VirtualizedMessageList/useNewMessageNotification.d.ts +1 -1
  31. package/dist/components/MessageList/hooks/VirtualizedMessageList/usePrependMessagesCount.js +2 -2
  32. package/dist/components/MessageList/hooks/VirtualizedMessageList/useScrollToBottomOnNewMessage.js +1 -1
  33. package/dist/components/MessageList/hooks/useMarkRead.d.ts +2 -4
  34. package/dist/components/MessageList/hooks/useMarkRead.js +14 -16
  35. package/dist/context/VirtualizedMessageListContext.d.ts +13 -0
  36. package/dist/context/VirtualizedMessageListContext.js +7 -0
  37. package/dist/experimental/index.browser.cjs.map +2 -2
  38. package/dist/experimental/index.node.cjs.map +2 -2
  39. package/dist/index.browser.cjs +572 -178
  40. package/dist/index.browser.cjs.map +4 -4
  41. package/dist/index.node.cjs +836 -197
  42. package/dist/index.node.cjs.map +4 -4
  43. package/package.json +12 -15
@@ -7,7 +7,7 @@ export const WaveProgressBar = ({ amplitudesCount = 40, progress = 0, relativeAm
7
7
  const isDragging = useRef(false);
8
8
  const [root, setRoot] = useState(null);
9
9
  const [trackAxisX, setTrackAxisX] = useState();
10
- const lastRootWidth = useRef();
10
+ const lastRootWidth = useRef(undefined);
11
11
  const handleDragStart = (e) => {
12
12
  e.preventDefault();
13
13
  if (!progressIndicator)
@@ -13,7 +13,7 @@ type AudioControllerParams = {
13
13
  playbackRates?: number[];
14
14
  };
15
15
  export declare const useAudioController: ({ durationSeconds, mimeType, playbackRates, }?: AudioControllerParams) => {
16
- audioRef: import("react").MutableRefObject<HTMLAudioElement | null>;
16
+ audioRef: import("react").RefObject<HTMLAudioElement | null>;
17
17
  canPlayRecord: boolean;
18
18
  increasePlaybackRate: () => void;
19
19
  isPlaying: boolean;
@@ -13,7 +13,7 @@ export const useAudioController = ({ durationSeconds, mimeType, playbackRates =
13
13
  const [canPlayRecord, setCanPlayRecord] = useState(true);
14
14
  const [secondsElapsed, setSecondsElapsed] = useState(0);
15
15
  const [playbackRateIndex, setPlaybackRateIndex] = useState(0);
16
- const playTimeout = useRef();
16
+ const playTimeout = useRef(undefined);
17
17
  const audioRef = useRef(null);
18
18
  const registerError = useCallback((e) => {
19
19
  logError(e);
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import Textarea from 'react-textarea-autosize';
4
4
  import getCaretCoordinates from 'textarea-caret';
5
- import { isValidElementType } from 'react-is';
6
5
  import clsx from 'clsx';
7
6
  import { List as DefaultSuggestionList } from './List';
8
7
  import { DEFAULT_CARET_POSITION, defaultScrollToItem, errorMessage, triggerPropsCheck, } from './utils';
@@ -242,9 +241,6 @@ export class ReactTextareaAutocomplete extends React.Component {
242
241
  if (!Array.isArray(data)) {
243
242
  throw new Error('Trigger provider has to provide an array!');
244
243
  }
245
- if (!isValidElementType(component)) {
246
- throw new Error('Component should be defined!');
247
- }
248
244
  // throw away if we resolved old trigger
249
245
  if (currentTrigger !== this.state.currentTrigger)
250
246
  return;
@@ -1,4 +1,3 @@
1
- import { isValidElementType } from 'react-is';
2
1
  export const DEFAULT_CARET_POSITION = 'next';
3
2
  export function defaultScrollToItem(container, item) {
4
3
  if (!item)
@@ -26,10 +25,7 @@ export const triggerPropsCheck = ({ trigger }) => {
26
25
  }
27
26
  // $FlowFixMe
28
27
  const triggerSetting = settings;
29
- const { callback, component, dataProvider, output } = triggerSetting;
30
- if (!isValidElementType(component)) {
31
- return Error('Invalid prop trigger: component should be defined.');
32
- }
28
+ const { callback, dataProvider, output } = triggerSetting;
33
29
  if (!dataProvider || typeof dataProvider !== 'function') {
34
30
  return Error('Invalid prop trigger: dataProvider should be defined.');
35
31
  }
@@ -4,7 +4,7 @@ import defaultsDeep from 'lodash.defaultsdeep';
4
4
  import throttle from 'lodash.throttle';
5
5
  import { nanoid } from 'nanoid';
6
6
  import clsx from 'clsx';
7
- import { channelReducer, initialState } from './channelState';
7
+ import { initialState, makeChannelReducer } from './channelState';
8
8
  import { useCreateChannelStateContext } from './hooks/useCreateChannelStateContext';
9
9
  import { useCreateTypingContext } from './hooks/useCreateTypingContext';
10
10
  import { useEditMessageHandler } from './hooks/useEditMessageHandler';
@@ -63,6 +63,7 @@ const ChannelInner = (props) => {
63
63
  const [quotedMessage, setQuotedMessage] = useState();
64
64
  const [channelUnreadUiState, _setChannelUnreadUiState] = useState();
65
65
  const notificationTimeouts = useRef([]);
66
+ const channelReducer = useMemo(() => makeChannelReducer(), []);
66
67
  const [state, dispatch] = useReducer(channelReducer,
67
68
  // channel.initialized === false if client.channel().query() was not called, e.g. ChannelList is not used
68
69
  // => Channel will call channel.watch() in useLayoutEffect => state.loading is used to signal the watch() call state
@@ -73,7 +74,7 @@ const ChannelInner = (props) => {
73
74
  });
74
75
  const isMounted = useIsMounted();
75
76
  const originalTitle = useRef('');
76
- const lastRead = useRef();
77
+ const lastRead = useRef(undefined);
77
78
  const online = useRef(true);
78
79
  const channelCapabilitiesArray = channel.data?.own_capabilities;
79
80
  const throttledCopyStateFromChannel = throttle(() => dispatch({ channel, type: 'copyStateFromChannelOnEvent' }), 500, {
@@ -1,4 +1,3 @@
1
- import type { Reducer } from 'react';
2
1
  import type { Channel, MessageResponse, ChannelState as StreamChannelState } from 'stream-chat';
3
2
  import type { ChannelState, StreamMessage } from '../../context/ChannelStateContext';
4
3
  import type { DefaultStreamChatGenerics } from '../../types/types';
@@ -61,8 +60,7 @@ export type ChannelStateReducerAction<StreamChatGenerics extends DefaultStreamCh
61
60
  } | {
62
61
  type: 'jumpToLatestMessage';
63
62
  };
64
- export type ChannelStateReducer<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = Reducer<ChannelState<StreamChatGenerics>, ChannelStateReducerAction<StreamChatGenerics>>;
65
- export declare const channelReducer: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(state: ChannelState<StreamChatGenerics>, action: ChannelStateReducerAction<StreamChatGenerics>) => ChannelState<StreamChatGenerics>;
63
+ export declare const makeChannelReducer: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>() => (state: ChannelState<StreamChatGenerics>, action: ChannelStateReducerAction<StreamChatGenerics>) => ChannelState<StreamChatGenerics>;
66
64
  export declare const initialState: {
67
65
  error: null;
68
66
  hasMore: boolean;
@@ -1,4 +1,4 @@
1
- export const channelReducer = (state, action) => {
1
+ export const makeChannelReducer = () => (state, action) => {
2
2
  switch (action.type) {
3
3
  case 'closeThread': {
4
4
  return {
@@ -1,2 +1,2 @@
1
1
  /// <reference types="react" />
2
- export declare const useIsMounted: () => import("react").MutableRefObject<boolean>;
2
+ export declare const useIsMounted: () => import("react").RefObject<boolean>;
@@ -245,8 +245,8 @@ export const useChannelListShapeDefaults = () => {
245
245
  };
246
246
  export const usePrepareShapeHandlers = ({ allowNewMessagesFromUnfilteredChannels, customHandleChannelListShape, filters, lockChannelOrder, onAddedToChannel, onChannelDeleted, onChannelHidden, onChannelTruncated, onChannelUpdated, onChannelVisible, onMessageNew, onMessageNewHandler, onRemovedFromChannel, setChannels, sort, }) => {
247
247
  const defaults = useChannelListShapeDefaults();
248
- const defaultHandleChannelListShapeRef = useRef();
249
- const customHandleChannelListShapeRef = useRef();
248
+ const defaultHandleChannelListShapeRef = useRef(undefined);
249
+ const customHandleChannelListShapeRef = useRef(undefined);
250
250
  customHandleChannelListShapeRef.current = (event) => {
251
251
  // @ts-expect-error can't use ReturnType<typeof useChannelListShapeDefaults<SCG>> until we upgrade prettier to at least v2.7.0
252
252
  customHandleChannelListShape?.({ defaults, event, setChannels });
@@ -1 +1 @@
1
- export declare const useMobileNavigation: (channelListRef: React.RefObject<HTMLDivElement>, navOpen: boolean, closeMobileNav?: () => void) => void;
1
+ export declare const useMobileNavigation: (channelListRef: React.RefObject<HTMLDivElement | null>, navOpen: boolean, closeMobileNav?: () => void) => void;
@@ -9,7 +9,7 @@ export const usePaginatedChannels = (client, filters, sort, options, activeChann
9
9
  const { channelsQueryState: { error, setError, setQueryInProgress }, } = useChatContext('usePaginatedChannels');
10
10
  const [channels, setChannels] = useState([]);
11
11
  const [hasNextPage, setHasNextPage] = useState(true);
12
- const lastRecoveryTimestamp = useRef();
12
+ const lastRecoveryTimestamp = useRef(undefined);
13
13
  const recoveryThrottleInterval = recoveryThrottleIntervalMs < MIN_RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS
14
14
  ? MIN_RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS
15
15
  : recoveryThrottleIntervalMs ?? RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS;
@@ -11,7 +11,7 @@ export type SearchBarController = {
11
11
  /** Flag determining whether the search input is focused */
12
12
  inputIsFocused: boolean;
13
13
  /** Ref object for the input wrapper in the SearchBar */
14
- searchBarRef: React.RefObject<HTMLDivElement>;
14
+ searchBarRef: React.RefObject<HTMLDivElement | null>;
15
15
  };
16
16
  export type AdditionalSearchBarProps = {
17
17
  /** Application menu to be displayed when clicked on MenuIcon */
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  export type SearchInputController = {
3
3
  /** Clears the channel search state */
4
4
  clearState: () => void;
5
- inputRef: React.RefObject<HTMLInputElement>;
5
+ inputRef: React.RefObject<HTMLInputElement | null>;
6
6
  /** Search input change handler */
7
7
  onSearch: React.ChangeEventHandler<HTMLInputElement>;
8
8
  /** Current search string */
@@ -9,7 +9,7 @@ export const useChannelSearch = ({ channelType = 'messaging', clearSearchOnClick
9
9
  const [query, setQuery] = useState('');
10
10
  const [results, setResults] = useState([]);
11
11
  const [searching, setSearching] = useState(false);
12
- const searchQueryPromiseInProgress = useRef();
12
+ const searchQueryPromiseInProgress = useRef(undefined);
13
13
  const shouldIgnoreQueryResults = useRef(false);
14
14
  const inputRef = useRef(null);
15
15
  const searchBarRef = useRef(null);
@@ -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.9.0-${userAgent}`);
31
+ client.setUserAgent(`stream-chat-react-12.10.0-${userAgent}`);
32
32
  }
33
33
  client.threads.registerSubscriptions();
34
34
  client.polls.registerSubscriptions();
@@ -1,5 +1,3 @@
1
1
  import React from 'react';
2
2
  export type BaseImageProps = React.ComponentPropsWithRef<'img'>;
3
- export declare const BaseImage: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "ref"> & {
4
- ref?: ((instance: HTMLImageElement | null) => void) | React.RefObject<HTMLImageElement> | null | undefined;
5
- }, "ref"> & React.RefAttributes<HTMLImageElement>>;
3
+ export declare const BaseImage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "ref"> & React.RefAttributes<HTMLImageElement>>;
@@ -29,11 +29,19 @@ const UnMemoizedGallery = (props) => {
29
29
  images[lastImageIndexInPreview].image_url ||
30
30
  images[lastImageIndexInPreview].thumb_url})`,
31
31
  ...image.style,
32
- }, ...(innerRefs?.current && { ref: (r) => (innerRefs.current[i] = r) }) },
32
+ }, ...(innerRefs?.current && {
33
+ ref: (r) => {
34
+ innerRefs.current[i] = r;
35
+ },
36
+ }) },
33
37
  React.createElement("p", null, t('{{ imageCount }} more', {
34
38
  imageCount: images.length - countImagesDisplayedInPreview,
35
39
  })))) : (React.createElement("button", { className: 'str-chat__gallery-image', "data-testid": 'gallery-image', key: `gallery-image-${i}`, onClick: () => toggleModal(i) },
36
- React.createElement(BaseImage, { alt: image?.fallback || imageFallbackTitle, src: sanitizeUrl(image.previewUrl || image.image_url || image.thumb_url), style: image.style, title: image?.fallback || imageFallbackTitle, ...(innerRefs?.current && { ref: (r) => (innerRefs.current[i] = r) }) }))));
40
+ React.createElement(BaseImage, { alt: image?.fallback || imageFallbackTitle, src: sanitizeUrl(image.previewUrl || image.image_url || image.thumb_url), style: image.style, title: image?.fallback || imageFallbackTitle, ...(innerRefs?.current && {
41
+ ref: (r) => {
42
+ innerRefs.current[i] = r;
43
+ },
44
+ }) }))));
37
45
  const className = clsx('str-chat__gallery', {
38
46
  'str-chat__gallery--square': images.length > lastImageIndexInPreview,
39
47
  'str-chat__gallery-two-rows': images.length > 2,
@@ -18,8 +18,9 @@ export const ModalGallery = (props) => {
18
18
  originalAlt: t('User uploaded content'),
19
19
  source: imageSrc,
20
20
  };
21
- }),
22
- // eslint-disable-next-line react-hooks/exhaustive-deps
23
- [images]);
24
- return (React.createElement(ImageGallery, { items: formattedArray, renderItem: renderItem, showIndex: true, showPlayButton: false, showThumbnails: false, startIndex: index }));
21
+ }), [images, t]);
22
+ return (
23
+ // ignore the TS error as react-image-gallery was on @types/react@18 while stream-chat-react being upgraded to React 19 (https://github.com/xiaolin/react-image-gallery/issues/809)
24
+ // @ts-expect-error
25
+ React.createElement(ImageGallery, { items: formattedArray, renderItem: renderItem, showIndex: true, showPlayButton: false, showThumbnails: false, startIndex: index }));
25
26
  };
@@ -26,10 +26,10 @@ export const InfiniteScroll = (props) => {
26
26
  const loadPreviousPageFn = loadPreviousPage || loadMore;
27
27
  const hasNextPageFlag = hasNextPage || hasMoreNewer;
28
28
  const hasPreviousPageFlag = hasPreviousPage || hasMore;
29
- const scrollComponent = useRef();
30
- const previousOffset = useRef();
31
- const previousReverseOffset = useRef();
32
- const scrollListenerRef = useRef();
29
+ const scrollComponent = useRef(undefined);
30
+ const previousOffset = useRef(undefined);
31
+ const previousReverseOffset = useRef(undefined);
32
+ const scrollListenerRef = useRef(undefined);
33
33
  scrollListenerRef.current = () => {
34
34
  const element = scrollComponent.current;
35
35
  if (!element || element.offsetParent === null) {
@@ -11,7 +11,7 @@ export declare function useMessageActionsBoxPopper<T extends HTMLElement>({ open
11
11
  [key: string]: string;
12
12
  } | undefined;
13
13
  };
14
- popperElementRef: import("react").RefObject<T>;
14
+ popperElementRef: import("react").RefObject<T | null>;
15
15
  styles: {
16
16
  [key: string]: import("react").CSSProperties;
17
17
  };
@@ -6,5 +6,5 @@ import type { EnrichURLsController } from './useLinkPreviews';
6
6
  export declare const useMessageInputText: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, V extends CustomTrigger = CustomTrigger>(props: MessageInputProps<StreamChatGenerics, V>, state: MessageInputState<StreamChatGenerics>, dispatch: React.Dispatch<MessageInputReducerAction<StreamChatGenerics>>, findAndEnqueueURLsToEnrich?: EnrichURLsController['findAndEnqueueURLsToEnrich']) => {
7
7
  handleChange: import("react").ChangeEventHandler<HTMLTextAreaElement>;
8
8
  insertText: (textToInsert: string) => void;
9
- textareaRef: import("react").MutableRefObject<HTMLTextAreaElement | undefined>;
9
+ textareaRef: import("react").RefObject<HTMLTextAreaElement | undefined>;
10
10
  };
@@ -5,7 +5,7 @@ export const useMessageInputText = (props, state, dispatch, findAndEnqueueURLsTo
5
5
  const { channel } = useChannelStateContext('useMessageInputText');
6
6
  const { additionalTextareaProps, focus, parent, publishTypingEvent = true } = props;
7
7
  const { text } = state;
8
- const textareaRef = useRef();
8
+ const textareaRef = useRef(undefined);
9
9
  // Focus
10
10
  useEffect(() => {
11
11
  if (focus && textareaRef.current) {
@@ -13,7 +13,7 @@ export const useMessageInputText = (props, state, dispatch, findAndEnqueueURLsTo
13
13
  }
14
14
  }, [focus]);
15
15
  // Text + cursor position
16
- const newCursorPosition = useRef();
16
+ const newCursorPosition = useRef(undefined);
17
17
  const insertText = useCallback((textToInsert) => {
18
18
  const { maxLength } = additionalTextareaProps || {};
19
19
  if (!textareaRef.current) {
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  // todo: provide start timestamp
3
3
  export const useTimeElapsed = ({ startOnMount } = {}) => {
4
4
  const [secondsElapsed, setSecondsElapsed] = useState(0);
5
- const updateInterval = useRef();
5
+ const updateInterval = useRef(undefined);
6
6
  const startCounter = useCallback(() => {
7
7
  if (updateInterval.current)
8
8
  return;
@@ -42,7 +42,6 @@ const MessageListWithContext = (props) => {
42
42
  useMarkRead({
43
43
  isMessageListScrolledToBottom,
44
44
  messageListIsThread: threadList,
45
- unreadCount: channelUnreadUiState?.unread_messages ?? 0,
46
45
  wasMarkedUnread: !!channelUnreadUiState?.first_unread_message_id,
47
46
  });
48
47
  const { messageGroupStyles, messages: enrichedMessages } = useEnrichedMessages({
@@ -25,7 +25,7 @@ export type VirtuosoContext<StreamChatGenerics extends DefaultStreamChatGenerics
25
25
  /** The original message list enriched with date separators, omitted deleted messages or giphy previews. */
26
26
  processedMessages: StreamMessage<StreamChatGenerics>[];
27
27
  /** Instance of VirtuosoHandle object providing the API to navigate in the virtualized list by various scroll actions. */
28
- virtuosoRef: RefObject<VirtuosoHandle>;
28
+ virtuosoRef: RefObject<VirtuosoHandle | null>;
29
29
  /** Message id which was marked as unread. ALl the messages following this message are considered unrea. */
30
30
  firstUnreadMessageId?: string;
31
31
  lastReadDate?: Date;
@@ -19,6 +19,7 @@ import { useChannelActionContext, } from '../../context/ChannelActionContext';
19
19
  import { useChannelStateContext, } from '../../context/ChannelStateContext';
20
20
  import { useChatContext } from '../../context/ChatContext';
21
21
  import { useComponentContext } from '../../context/ComponentContext';
22
+ import { VirtualizedMessageListContextProvider } from '../../context/VirtualizedMessageListContext';
22
23
  import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits';
23
24
  function captureResizeObserverExceededError(e) {
24
25
  if (e.message === 'ResizeObserver loop completed with undelivered notifications.' ||
@@ -122,7 +123,6 @@ const VirtualizedMessageListWithContext = (props) => {
122
123
  useMarkRead({
123
124
  isMessageListScrolledToBottom,
124
125
  messageListIsThread: !!threadList,
125
- unreadCount: channelUnreadUiState?.unread_messages ?? 0,
126
126
  wasMarkedUnread: !!channelUnreadUiState?.first_unread_message_id,
127
127
  });
128
128
  const scrollToBottom = useCallback(async () => {
@@ -192,7 +192,7 @@ const VirtualizedMessageListWithContext = (props) => {
192
192
  const dialogManagerId = threadList
193
193
  ? 'virtualized-message-list-dialog-manager-thread'
194
194
  : 'virtualized-message-list-dialog-manager';
195
- return (React.createElement(React.Fragment, null,
195
+ return (React.createElement(VirtualizedMessageListContextProvider, { value: { scrollToBottom } },
196
196
  React.createElement(MessageListMainPanel, null,
197
197
  React.createElement(DialogManagerProvider, { id: dialogManagerId },
198
198
  !threadList && showUnreadMessagesNotification && (React.createElement(UnreadMessagesNotification, { unreadCount: channelUnreadUiState?.unread_messages })),
@@ -8,7 +8,7 @@ export function useMessageListScrollManager(params) {
8
8
  offsetHeight: 0,
9
9
  scrollHeight: 0,
10
10
  });
11
- const messages = useRef();
11
+ const messages = useRef(undefined);
12
12
  const scrollTop = useRef(0);
13
13
  useLayoutEffect(() => {
14
14
  const prevMeasures = measures.current;
@@ -4,7 +4,7 @@ export const useMessageSetKey = ({ messages, }) => {
4
4
  * Logic to update the key of the virtuoso component when the list jumps to a new location.
5
5
  */
6
6
  const [messageSetKey, setMessageSetKey] = useState(+new Date());
7
- const firstMessageId = useRef();
7
+ const firstMessageId = useRef(undefined);
8
8
  useEffect(() => {
9
9
  const continuousSet = messages?.find((message) => message.id === firstMessageId.current);
10
10
  if (!continuousSet) {
@@ -2,7 +2,7 @@
2
2
  import type { StreamMessage } from '../../../../context/ChannelStateContext';
3
3
  import type { DefaultStreamChatGenerics } from '../../../../types/types';
4
4
  export declare function useNewMessageNotification<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(messages: StreamMessage<StreamChatGenerics>[], currentUserId: string | undefined, hasMoreNewer?: boolean): {
5
- atBottom: import("react").MutableRefObject<boolean>;
5
+ atBottom: import("react").RefObject<boolean>;
6
6
  isMessageListScrolledToBottom: boolean;
7
7
  newMessagesNotification: boolean;
8
8
  setIsMessageListScrolledToBottom: import("react").Dispatch<import("react").SetStateAction<boolean>>;
@@ -5,8 +5,8 @@ const STATUSES_EXCLUDED_FROM_PREPEND = {
5
5
  };
6
6
  export function usePrependedMessagesCount(messages, hasDateSeparator) {
7
7
  const firstRealMessageIndex = hasDateSeparator ? 1 : 0;
8
- const firstMessageOnFirstLoadedPage = useRef();
9
- const previousFirstMessageOnFirstLoadedPage = useRef();
8
+ const firstMessageOnFirstLoadedPage = useRef(undefined);
9
+ const previousFirstMessageOnFirstLoadedPage = useRef(undefined);
10
10
  const previousNumItemsPrepended = useRef(0);
11
11
  const numItemsPrepended = useMemo(() => {
12
12
  if (!messages || !messages.length) {
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  export const useScrollToBottomOnNewMessage = ({ messages, scrollToBottom, scrollToLatestMessageOnFocus, }) => {
3
3
  const [newMessagesReceivedInBackground, setNewMessagesReceivedInBackground] = useState(false);
4
- const scrollToBottomIfConfigured = useRef();
4
+ const scrollToBottomIfConfigured = useRef(undefined);
5
5
  scrollToBottomIfConfigured.current = (event) => {
6
6
  if (!scrollToLatestMessageOnFocus ||
7
7
  !newMessagesReceivedInBackground ||
@@ -1,8 +1,7 @@
1
- import { DefaultStreamChatGenerics } from '../../../types';
1
+ import type { DefaultStreamChatGenerics } from '../../../types';
2
2
  type UseMarkReadParams = {
3
3
  isMessageListScrolledToBottom: boolean;
4
4
  messageListIsThread: boolean;
5
- unreadCount: number;
6
5
  wasMarkedUnread?: boolean;
7
6
  };
8
7
  /**
@@ -12,8 +11,7 @@ type UseMarkReadParams = {
12
11
  * 3. the channel was not marked unread by the user
13
12
  * @param isMessageListScrolledToBottom
14
13
  * @param messageListIsThread
15
- * @param unreadCount
16
14
  * @param wasChannelMarkedUnread
17
15
  */
18
- export declare const useMarkRead: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ isMessageListScrolledToBottom, messageListIsThread, unreadCount, wasMarkedUnread, }: UseMarkReadParams) => void;
16
+ export declare const useMarkRead: <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ isMessageListScrolledToBottom, messageListIsThread, wasMarkedUnread, }: UseMarkReadParams) => void;
19
17
  export {};
@@ -1,5 +1,10 @@
1
- import { useEffect, useRef } from 'react';
1
+ import { useEffect } from 'react';
2
2
  import { useChannelActionContext, useChannelStateContext, useChatContext, } from '../../../context';
3
+ const hasReadLastMessage = (channel, userId) => {
4
+ const latestMessageIdInChannel = channel.state.latestMessages.slice(-1)[0]?.id;
5
+ const lastReadMessageIdServer = channel.state.read[userId]?.last_read_message_id;
6
+ return latestMessageIdInChannel === lastReadMessageIdServer;
7
+ };
3
8
  /**
4
9
  * Takes care of marking a channel read. The channel is read only if all the following applies:
5
10
  * 1. the message list is not rendered in a thread
@@ -7,29 +12,25 @@ import { useChannelActionContext, useChannelStateContext, useChatContext, } from
7
12
  * 3. the channel was not marked unread by the user
8
13
  * @param isMessageListScrolledToBottom
9
14
  * @param messageListIsThread
10
- * @param unreadCount
11
15
  * @param wasChannelMarkedUnread
12
16
  */
13
- export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread, unreadCount, wasMarkedUnread, }) => {
17
+ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread, wasMarkedUnread, }) => {
14
18
  const { client } = useChatContext('useMarkRead');
15
19
  const { markRead, setChannelUnreadUiState } = useChannelActionContext('useMarkRead');
16
20
  const { channel } = useChannelStateContext('useMarkRead');
17
- const previousRenderMessageListScrolledToBottom = useRef(isMessageListScrolledToBottom);
18
21
  useEffect(() => {
19
- const shouldMarkRead = (unreadMessages) => !document.hidden &&
22
+ const shouldMarkRead = () => !document.hidden &&
20
23
  !wasMarkedUnread &&
21
24
  !messageListIsThread &&
22
25
  isMessageListScrolledToBottom &&
23
- unreadMessages > 0;
26
+ client.user?.id &&
27
+ !hasReadLastMessage(channel, client.user.id);
24
28
  const onVisibilityChange = () => {
25
- if (shouldMarkRead(channel.countUnread()))
29
+ if (shouldMarkRead())
26
30
  markRead();
27
31
  };
28
32
  const handleMessageNew = (event) => {
29
- const isOwnMessage = event.user?.id && event.user.id === client.user?.id;
30
33
  const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
31
- if (isOwnMessage)
32
- return;
33
34
  if (!isMessageListScrolledToBottom || wasMarkedUnread || document.hidden) {
34
35
  setChannelUnreadUiState((prev) => {
35
36
  const previousUnreadCount = prev?.unread_messages ?? 0;
@@ -44,17 +45,15 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
44
45
  };
45
46
  });
46
47
  }
47
- else if (mainChannelUpdated && shouldMarkRead(channel.countUnread())) {
48
+ else if (mainChannelUpdated && shouldMarkRead()) {
48
49
  markRead();
49
50
  }
50
51
  };
51
52
  channel.on('message.new', handleMessageNew);
52
53
  document.addEventListener('visibilitychange', onVisibilityChange);
53
- const hasScrolledToBottom = previousRenderMessageListScrolledToBottom.current !== isMessageListScrolledToBottom &&
54
- isMessageListScrolledToBottom;
55
- if (hasScrolledToBottom && shouldMarkRead(channel.countUnread()))
54
+ if (shouldMarkRead()) {
56
55
  markRead();
57
- previousRenderMessageListScrolledToBottom.current = isMessageListScrolledToBottom;
56
+ }
58
57
  return () => {
59
58
  channel.off('message.new', handleMessageNew);
60
59
  document.removeEventListener('visibilitychange', onVisibilityChange);
@@ -66,7 +65,6 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
66
65
  markRead,
67
66
  messageListIsThread,
68
67
  setChannelUnreadUiState,
69
- unreadCount,
70
68
  wasMarkedUnread,
71
69
  ]);
72
70
  };
@@ -0,0 +1,13 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ export type VirtualizedMessageListContextValue = {
3
+ /** Function that scrolls the list to the bottom. */
4
+ scrollToBottom: () => void;
5
+ };
6
+ export declare const VirtualizedMessageListContext: React.Context<VirtualizedMessageListContextValue | undefined>;
7
+ /**
8
+ * Context provider for components rendered within the `VirtualizedMessageList`
9
+ */
10
+ export declare const VirtualizedMessageListContextProvider: ({ children, value, }: PropsWithChildren<{
11
+ value: VirtualizedMessageListContextValue;
12
+ }>) => React.JSX.Element;
13
+ export declare const useVirtualizedMessageListContext: () => VirtualizedMessageListContextValue;
@@ -0,0 +1,7 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ export const VirtualizedMessageListContext = createContext(undefined);
3
+ /**
4
+ * Context provider for components rendered within the `VirtualizedMessageList`
5
+ */
6
+ export const VirtualizedMessageListContextProvider = ({ children, value, }) => (React.createElement(VirtualizedMessageListContext.Provider, { value: value }, children));
7
+ export const useVirtualizedMessageListContext = () => useContext(VirtualizedMessageListContext);