stream-chat-react 12.11.0 → 12.12.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 (89) hide show
  1. package/dist/components/Channel/Channel.d.ts +1 -1
  2. package/dist/components/Channel/Channel.js +41 -26
  3. package/dist/components/Channel/channelState.d.ts +1 -1
  4. package/dist/components/Channel/channelState.js +2 -1
  5. package/dist/components/ChannelList/ChannelList.js +15 -9
  6. package/dist/components/ChannelList/hooks/useChannelListShape.js +4 -3
  7. package/dist/components/ChannelPreview/ChannelPreview.d.ts +2 -2
  8. package/dist/components/ChannelPreview/ChannelPreview.js +3 -4
  9. package/dist/components/ChannelPreview/ChannelPreviewMessenger.d.ts +1 -1
  10. package/dist/components/ChannelSearch/SearchBar.js +7 -8
  11. package/dist/components/ChannelSearch/SearchResults.js +8 -7
  12. package/dist/components/Chat/Chat.d.ts +5 -2
  13. package/dist/components/Chat/Chat.js +12 -2
  14. package/dist/components/Chat/hooks/useChat.js +1 -1
  15. package/dist/components/Chat/hooks/useCreateChatContext.js +3 -1
  16. package/dist/components/InfiniteScrollPaginator/InfiniteScroll.d.ts +1 -1
  17. package/dist/components/InfiniteScrollPaginator/InfiniteScroll.js +1 -1
  18. package/dist/components/InfiniteScrollPaginator/InfiniteScrollPaginator.d.ts +1 -0
  19. package/dist/components/InfiniteScrollPaginator/InfiniteScrollPaginator.js +8 -2
  20. package/dist/components/MediaRecorder/classes/MediaRecorderController.d.ts +6 -1
  21. package/dist/components/MediaRecorder/classes/MediaRecorderController.js +6 -8
  22. package/dist/components/Message/renderText/renderText.d.ts +3 -3
  23. package/dist/components/Message/renderText/renderText.js +3 -3
  24. package/dist/context/ChatContext.d.ts +3 -1
  25. package/dist/context/ComponentContext.d.ts +23 -0
  26. package/dist/experimental/Search/Search.d.ts +12 -0
  27. package/dist/experimental/Search/Search.js +25 -0
  28. package/dist/experimental/Search/SearchBar/SearchBar.d.ts +2 -0
  29. package/dist/experimental/Search/SearchBar/SearchBar.js +56 -0
  30. package/dist/experimental/Search/SearchBar/index.d.ts +1 -0
  31. package/dist/experimental/Search/SearchBar/index.js +1 -0
  32. package/dist/experimental/Search/SearchContext.d.ts +23 -0
  33. package/dist/experimental/Search/SearchContext.js +10 -0
  34. package/dist/experimental/Search/SearchResults/SearchResultItem.d.ts +19 -0
  35. package/dist/experimental/Search/SearchResults/SearchResultItem.js +62 -0
  36. package/dist/experimental/Search/SearchResults/SearchResults.d.ts +3 -0
  37. package/dist/experimental/Search/SearchResults/SearchResults.js +21 -0
  38. package/dist/experimental/Search/SearchResults/SearchResultsHeader.d.ts +3 -0
  39. package/dist/experimental/Search/SearchResults/SearchResultsHeader.js +31 -0
  40. package/dist/experimental/Search/SearchResults/SearchResultsPresearch.d.ts +6 -0
  41. package/dist/experimental/Search/SearchResults/SearchResultsPresearch.js +6 -0
  42. package/dist/experimental/Search/SearchResults/SearchSourceResultList.d.ts +9 -0
  43. package/dist/experimental/Search/SearchResults/SearchSourceResultList.js +22 -0
  44. package/dist/experimental/Search/SearchResults/SearchSourceResultListFooter.d.ts +3 -0
  45. package/dist/experimental/Search/SearchResults/SearchSourceResultListFooter.js +16 -0
  46. package/dist/experimental/Search/SearchResults/SearchSourceResults.d.ts +7 -0
  47. package/dist/experimental/Search/SearchResults/SearchSourceResults.js +21 -0
  48. package/dist/experimental/Search/SearchResults/SearchSourceResultsEmpty.d.ts +2 -0
  49. package/dist/experimental/Search/SearchResults/SearchSourceResultsEmpty.js +6 -0
  50. package/dist/experimental/Search/SearchResults/SearchSourceResultsHeader.d.ts +1 -0
  51. package/dist/experimental/Search/SearchResults/SearchSourceResultsHeader.js +1 -0
  52. package/dist/experimental/Search/SearchResults/SearchSourceResultsLoadingIndicator.d.ts +2 -0
  53. package/dist/experimental/Search/SearchResults/SearchSourceResultsLoadingIndicator.js +8 -0
  54. package/dist/experimental/Search/SearchResults/index.d.ts +9 -0
  55. package/dist/experimental/Search/SearchResults/index.js +9 -0
  56. package/dist/experimental/Search/SearchSourceResultsContext.d.ts +13 -0
  57. package/dist/experimental/Search/SearchSourceResultsContext.js +10 -0
  58. package/dist/experimental/Search/hooks/index.d.ts +2 -0
  59. package/dist/experimental/Search/hooks/index.js +2 -0
  60. package/dist/experimental/Search/hooks/useSearchFocusedMessage.d.ts +2 -0
  61. package/dist/experimental/Search/hooks/useSearchFocusedMessage.js +8 -0
  62. package/dist/experimental/Search/hooks/useSearchQueriesInProgress.d.ts +6 -0
  63. package/dist/experimental/Search/hooks/useSearchQueriesInProgress.js +22 -0
  64. package/dist/experimental/Search/index.d.ts +5 -0
  65. package/dist/experimental/Search/index.js +5 -0
  66. package/dist/experimental/index.browser.cjs +11286 -301
  67. package/dist/experimental/index.browser.cjs.map +4 -4
  68. package/dist/experimental/index.d.ts +1 -0
  69. package/dist/experimental/index.js +1 -0
  70. package/dist/experimental/index.node.cjs +13176 -301
  71. package/dist/experimental/index.node.cjs.map +4 -4
  72. package/dist/i18n/Streami18n.d.ts +7 -0
  73. package/dist/i18n/de.json +7 -0
  74. package/dist/i18n/en.json +7 -0
  75. package/dist/i18n/es.json +7 -0
  76. package/dist/i18n/fr.json +8 -1
  77. package/dist/i18n/hi.json +7 -0
  78. package/dist/i18n/it.json +7 -0
  79. package/dist/i18n/ja.json +7 -0
  80. package/dist/i18n/ko.json +7 -0
  81. package/dist/i18n/nl.json +7 -0
  82. package/dist/i18n/pt.json +8 -1
  83. package/dist/i18n/ru.json +7 -0
  84. package/dist/i18n/tr.json +7 -0
  85. package/dist/index.browser.cjs +15820 -14850
  86. package/dist/index.browser.cjs.map +4 -4
  87. package/dist/index.node.cjs +17734 -16886
  88. package/dist/index.node.cjs.map +4 -4
  89. package/package.json +6 -7
@@ -1,8 +1,8 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
- import { ChannelQueryOptions, EventAPIResponse, Message, MessageResponse, Channel as StreamChannel, StreamChat, UpdatedMessage } from 'stream-chat';
3
2
  import { OnMentionAction } from './hooks/useMentionsHandlers';
4
3
  import { LoadingErrorIndicatorProps } from '../Loading';
5
4
  import { ComponentContextValue, StreamMessage } from '../../context';
5
+ import type { ChannelQueryOptions, EventAPIResponse, Message, MessageResponse, Channel as StreamChannel, StreamChat, UpdatedMessage } from 'stream-chat';
6
6
  import type { MessageInputProps } from '../MessageInput';
7
7
  import type { ChannelUnreadUiState, CustomTrigger, DefaultStreamChatGenerics, GiphyVersions, ImageAttachmentSizeHandler, SendMessageOptions, UpdateMessageOptions, VideoAttachmentSizeHandler } from '../../types/types';
8
8
  import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews';
@@ -14,14 +14,15 @@ import { LoadingErrorIndicator as DefaultLoadingErrorIndicator, } from '../Loadi
14
14
  import { LoadingChannel as DefaultLoadingIndicator } from './LoadingChannel';
15
15
  import { DropzoneProvider } from '../MessageInput/DropzoneProvider';
16
16
  import { ChannelActionProvider, ChannelStateProvider, TypingProvider, useChatContext, useTranslationContext, WithComponents, } from '../../context';
17
+ import { CHANNEL_CONTAINER_ID } from './constants';
17
18
  import { DEFAULT_HIGHLIGHT_DURATION, DEFAULT_INITIAL_CHANNEL_PAGE_SIZE, DEFAULT_JUMP_TO_PAGE_SIZE, DEFAULT_NEXT_CHANNEL_PAGE_SIZE, DEFAULT_THREAD_PAGE_SIZE, } from '../../constants/limits';
18
19
  import { hasMoreMessagesProbably } from '../MessageList';
19
20
  import { getChatContainerClass, useChannelContainerClasses, useImageFlagEmojisOnWindowsClass, } from './hooks/useChannelContainerClasses';
20
21
  import { findInMsgSetByDate, findInMsgSetById, makeAddNotifications } from './utils';
22
+ import { useThreadContext } from '../Threads';
21
23
  import { getChannel } from '../../utils';
22
24
  import { getImageAttachmentConfiguration, getVideoAttachmentConfiguration, } from '../Attachment/attachment-sizing';
23
- import { useThreadContext } from '../Threads';
24
- import { CHANNEL_CONTAINER_ID } from './constants';
25
+ import { useSearchFocusedMessage } from '../../experimental/Search/hooks';
25
26
  const isUserResponseArray = (output) => output[0]?.id != null;
26
27
  const ChannelContainer = ({ children, className: additionalClassName, ...props }) => {
27
28
  const { customClasses, theme } = useChatContext('Channel');
@@ -53,7 +54,7 @@ const ChannelInner = (props) => {
53
54
  const channelQueryOptions = useMemo(() => defaultsDeep(propChannelQueryOptions, {
54
55
  messages: { limit: DEFAULT_INITIAL_CHANNEL_PAGE_SIZE },
55
56
  }), [propChannelQueryOptions]);
56
- const { client, customClasses, latestMessageDatesByChannels, mutes } = useChatContext('Channel');
57
+ const { client, customClasses, latestMessageDatesByChannels, mutes, searchController } = useChatContext('Channel');
57
58
  const { t } = useTranslationContext('Channel');
58
59
  const chatContainerClass = getChatContainerClass(customClasses?.chatContainer);
59
60
  const windowsEmojiClass = useImageFlagEmojisOnWindowsClass();
@@ -72,10 +73,12 @@ const ChannelInner = (props) => {
72
73
  hasMore: channel.state.messagePagination.hasPrev,
73
74
  loading: !channel.initialized,
74
75
  });
76
+ const jumpToMessageFromSearch = useSearchFocusedMessage();
75
77
  const isMounted = useIsMounted();
76
78
  const originalTitle = useRef('');
77
79
  const lastRead = useRef(undefined);
78
80
  const online = useRef(true);
81
+ const clearHighlightedMessageTimeoutId = useRef(null);
79
82
  const channelCapabilitiesArray = channel.data?.own_capabilities;
80
83
  const throttledCopyStateFromChannel = throttle(() => dispatch({ channel, type: 'copyStateFromChannelOnEvent' }), 500, {
81
84
  leading: true,
@@ -282,6 +285,28 @@ const ChannelInner = (props) => {
282
285
  if (message)
283
286
  dispatch({ message, type: 'setThread' });
284
287
  }, [state.messages, state.thread]);
288
+ const handleHighlightedMessageChange = useCallback(({ highlightDuration, highlightedMessageId, }) => {
289
+ dispatch({
290
+ channel,
291
+ highlightedMessageId,
292
+ type: 'jumpToMessageFinished',
293
+ });
294
+ if (clearHighlightedMessageTimeoutId.current) {
295
+ clearTimeout(clearHighlightedMessageTimeoutId.current);
296
+ }
297
+ clearHighlightedMessageTimeoutId.current = setTimeout(() => {
298
+ if (searchController._internalState.getLatestValue().focusedMessage) {
299
+ searchController._internalState.partialNext({ focusedMessage: undefined });
300
+ }
301
+ clearHighlightedMessageTimeoutId.current = null;
302
+ dispatch({ type: 'clearHighlightedMessage' });
303
+ }, highlightDuration ?? DEFAULT_HIGHLIGHT_DURATION);
304
+ }, [channel, searchController]);
305
+ useEffect(() => {
306
+ if (!jumpToMessageFromSearch?.id)
307
+ return;
308
+ handleHighlightedMessageChange({ highlightedMessageId: jumpToMessageFromSearch.id });
309
+ }, [jumpToMessageFromSearch, handleHighlightedMessageChange]);
285
310
  /** MESSAGE */
286
311
  // Adds a temporary notification to message list, will be removed after 5 seconds
287
312
  const addNotification = useMemo(() => makeAddNotifications(setNotifications, notificationTimeouts.current), []);
@@ -351,24 +376,15 @@ const ChannelInner = (props) => {
351
376
  });
352
377
  return queryResponse.messages.length;
353
378
  };
354
- const clearHighlightedMessageTimeoutId = useRef(null);
355
379
  const jumpToMessage = useCallback(async (messageId, messageLimit = DEFAULT_JUMP_TO_PAGE_SIZE, highlightDuration = DEFAULT_HIGHLIGHT_DURATION) => {
356
380
  dispatch({ loadingMore: true, type: 'setLoadingMore' });
357
381
  await channel.state.loadMessageIntoState(messageId, undefined, messageLimit);
358
382
  loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
359
- dispatch({
360
- hasMoreNewer: channel.state.messagePagination.hasNext,
383
+ handleHighlightedMessageChange({
384
+ highlightDuration,
361
385
  highlightedMessageId: messageId,
362
- type: 'jumpToMessageFinished',
363
386
  });
364
- if (clearHighlightedMessageTimeoutId.current) {
365
- clearTimeout(clearHighlightedMessageTimeoutId.current);
366
- }
367
- clearHighlightedMessageTimeoutId.current = setTimeout(() => {
368
- clearHighlightedMessageTimeoutId.current = null;
369
- dispatch({ type: 'clearHighlightedMessage' });
370
- }, highlightDuration);
371
- }, [channel, loadMoreFinished]);
387
+ }, [channel, handleHighlightedMessageChange, loadMoreFinished]);
372
388
  const jumpToLatestMessage = useCallback(async () => {
373
389
  await channel.state.loadMessageIntoState('latest');
374
390
  loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
@@ -468,19 +484,18 @@ const ChannelInner = (props) => {
468
484
  first_unread_message_id: firstUnreadMessageId,
469
485
  last_read_message_id: lastReadMessageId,
470
486
  });
471
- dispatch({
472
- hasMoreNewer: channel.state.messagePagination.hasNext,
487
+ handleHighlightedMessageChange({
488
+ highlightDuration,
473
489
  highlightedMessageId: firstUnreadMessageId,
474
- type: 'jumpToMessageFinished',
475
490
  });
476
- if (clearHighlightedMessageTimeoutId.current) {
477
- clearTimeout(clearHighlightedMessageTimeoutId.current);
478
- }
479
- clearHighlightedMessageTimeoutId.current = setTimeout(() => {
480
- clearHighlightedMessageTimeoutId.current = null;
481
- dispatch({ type: 'clearHighlightedMessage' });
482
- }, highlightDuration);
483
- }, [addNotification, channel, loadMoreFinished, t, channelUnreadUiState]);
491
+ }, [
492
+ addNotification,
493
+ channel,
494
+ handleHighlightedMessageChange,
495
+ loadMoreFinished,
496
+ t,
497
+ channelUnreadUiState,
498
+ ]);
484
499
  const deleteMessage = useCallback(async (message) => {
485
500
  if (!message?.id) {
486
501
  throw new Error('Cannot delete a message - missing message ID.');
@@ -13,7 +13,7 @@ export type ChannelStateReducerAction<StreamChatGenerics extends DefaultStreamCh
13
13
  channel: Channel<StreamChatGenerics>;
14
14
  type: 'copyStateFromChannelOnEvent';
15
15
  } | {
16
- hasMoreNewer: boolean;
16
+ channel: Channel<StreamChatGenerics>;
17
17
  highlightedMessageId: string;
18
18
  type: 'jumpToMessageFinished';
19
19
  } | {
@@ -59,8 +59,9 @@ export const makeChannelReducer = () => (state, action) => {
59
59
  case 'jumpToMessageFinished': {
60
60
  return {
61
61
  ...state,
62
- hasMoreNewer: action.hasMoreNewer,
62
+ hasMoreNewer: action.channel.state.messagePagination.hasNext,
63
63
  highlightedMessageId: action.highlightedMessageId,
64
+ messages: action.channel.state.messages,
64
65
  };
65
66
  }
66
67
  case 'clearHighlightedMessage': {
@@ -4,6 +4,7 @@ import { ChannelListMessenger } from './ChannelListMessenger';
4
4
  import { useConnectionRecoveredListener } from './hooks/useConnectionRecoveredListener';
5
5
  import { useMobileNavigation } from './hooks/useMobileNavigation';
6
6
  import { usePaginatedChannels, } from './hooks/usePaginatedChannels';
7
+ import { useChannelListShape, usePrepareShapeHandlers, } from './hooks/useChannelListShape';
7
8
  import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils';
8
9
  import { Avatar as DefaultAvatar } from '../Avatar';
9
10
  import { ChannelPreview, } from '../ChannelPreview/ChannelPreview';
@@ -12,18 +13,24 @@ import { EmptyStateIndicator as DefaultEmptyStateIndicator, } from '../EmptyStat
12
13
  import { LoadingChannels } from '../Loading/LoadingChannels';
13
14
  import { LoadMorePaginator } from '../LoadMore/LoadMorePaginator';
14
15
  import { NullComponent } from '../UtilityComponents';
15
- import { ChannelListContextProvider } from '../../context';
16
+ import { ChannelListContextProvider, useComponentContext, } from '../../context';
16
17
  import { useChatContext } from '../../context/ChatContext';
17
- import { useChannelListShape, usePrepareShapeHandlers, } from './hooks/useChannelListShape';
18
+ import { useStateStore } from '../../store';
18
19
  const DEFAULT_FILTERS = {};
19
20
  const DEFAULT_OPTIONS = {};
20
21
  const DEFAULT_SORT = {};
22
+ const searchControllerStateSelector = (nextValue) => ({
23
+ searchIsActive: nextValue.isActive,
24
+ });
21
25
  const UnMemoizedChannelList = (props) => {
22
26
  const { additionalChannelSearchProps, allowNewMessagesFromUnfilteredChannels = true, Avatar = DefaultAvatar, channelRenderFilterFn, ChannelSearch = DefaultChannelSearch, customActiveChannel, customQueryChannels, EmptyStateIndicator = DefaultEmptyStateIndicator, filters = {}, getLatestMessagePreview, List = ChannelListMessenger, LoadingErrorIndicator = NullComponent, LoadingIndicator = LoadingChannels, lockChannelOrder = false, onAddedToChannel, onChannelDeleted, onChannelHidden, onChannelTruncated, onChannelUpdated, onChannelVisible, onMessageNew, onMessageNewHandler, onRemovedFromChannel, options, Paginator = LoadMorePaginator, Preview, recoveryThrottleIntervalMs, renderChannels, sendChannelsToList = false, setActiveChannelOnMount = true, showChannelSearch = false, sort = DEFAULT_SORT, watchers = {}, } = props;
23
- const { channel, channelsQueryState, client, closeMobileNav, customClasses, navOpen = false, setActiveChannel, theme, useImageFlagEmojisOnWindows, } = useChatContext('ChannelList');
27
+ const { channel, channelsQueryState, client, closeMobileNav, customClasses, navOpen = false, searchController, setActiveChannel, theme, useImageFlagEmojisOnWindows, } = useChatContext('ChannelList');
28
+ const { Search } = useComponentContext(); // FIXME: us component context to retrieve ChannelPreview UI components too
24
29
  const channelListRef = useRef(null);
25
30
  const [channelUpdateCount, setChannelUpdateCount] = useState(0);
26
31
  const [searchActive, setSearchActive] = useState(false);
32
+ // Indicator relevant when Search component that relies on SearchController is used
33
+ const { searchIsActive } = useStateStore(searchController.state, searchControllerStateSelector);
27
34
  /**
28
35
  * Set a channel with id {customActiveChannel} as active and move it to the top of the list.
29
36
  * If customActiveChannel prop is absent, then set the first channel in list as active channel.
@@ -65,13 +72,11 @@ const UnMemoizedChannelList = (props) => {
65
72
  const onSearch = useCallback((event) => {
66
73
  setSearchActive(!!event.target.value);
67
74
  additionalChannelSearchProps?.onSearch?.(event);
68
- // eslint-disable-next-line react-hooks/exhaustive-deps
69
- }, []);
75
+ }, [additionalChannelSearchProps]);
70
76
  const onSearchExit = useCallback(() => {
71
77
  setSearchActive(false);
72
78
  additionalChannelSearchProps?.onSearchExit?.();
73
- // eslint-disable-next-line react-hooks/exhaustive-deps
74
- }, []);
79
+ }, [additionalChannelSearchProps]);
75
80
  const { channels, hasNextPage, loadNextPage, setChannels } = usePaginatedChannels(client, filters || DEFAULT_FILTERS, sort || DEFAULT_SORT, options || DEFAULT_OPTIONS, activeChannelHandler, recoveryThrottleIntervalMs, customQueryChannels);
76
81
  const loadedChannels = channelRenderFilterFn
77
82
  ? channelRenderFilterFn(channels)
@@ -132,10 +137,11 @@ const UnMemoizedChannelList = (props) => {
132
137
  'str-chat--windows-flags': useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/),
133
138
  [`${baseClass}--open`]: navOpen,
134
139
  });
135
- const showChannelList = !searchActive || additionalChannelSearchProps?.popupResults;
140
+ const showChannelList = (!searchActive && !searchIsActive) || additionalChannelSearchProps?.popupResults;
136
141
  return (React.createElement(ChannelListContextProvider, { value: { channels, setChannels } },
137
142
  React.createElement("div", { className: className, ref: channelListRef },
138
- showChannelSearch && (React.createElement(ChannelSearch, { onSearch: onSearch, onSearchExit: onSearchExit, setChannels: setChannels, ...additionalChannelSearchProps })),
143
+ showChannelSearch &&
144
+ (Search ? (React.createElement(Search, { directMessagingChannelType: additionalChannelSearchProps?.channelType, disabled: additionalChannelSearchProps?.disabled, exitSearchOnInputBlur: additionalChannelSearchProps?.clearSearchOnClickOutside, placeholder: additionalChannelSearchProps?.placeholder })) : (React.createElement(ChannelSearch, { onSearch: onSearch, onSearchExit: onSearchExit, setChannels: setChannels, ...additionalChannelSearchProps }))),
139
145
  showChannelList && (React.createElement(List, { error: channelsQueryState.error, loadedChannels: sendChannelsToList ? loadedChannels : undefined, loading: !!channelsQueryState.queryInProgress &&
140
146
  ['reload', 'uninitialized'].includes(channelsQueryState.queryInProgress), LoadingErrorIndicator: LoadingErrorIndicator, LoadingIndicator: LoadingIndicator, setChannels: setChannels }, !loadedChannels?.length ? (React.createElement(EmptyStateIndicator, { listType: 'channel' })) : (React.createElement(Paginator, { hasNextPage: hasNextPage, isLoading: channelsQueryState.queryInProgress === 'load-more', loadNextPage: loadNextPage }, renderChannels
141
147
  ? renderChannels(loadedChannels, renderChannel)
@@ -120,6 +120,10 @@ export const useChannelListShapeDefaults = () => {
120
120
  const channelId = event.channel_id;
121
121
  const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
122
122
  const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
123
+ // `pinned_at` nor `archived` properties are set or channel list order is locked, return early
124
+ if ((!considerPinnedChannels && !considerArchivedChannels) || lockChannelOrder) {
125
+ return;
126
+ }
123
127
  const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });
124
128
  setChannels((currentChannels) => {
125
129
  const targetChannel = client.channel(channelType, channelId);
@@ -128,9 +132,6 @@ export const useChannelListShapeDefaults = () => {
128
132
  const targetChannelExistsWithinList = targetChannelIndex >= 0;
129
133
  const isTargetChannelArchived = isChannelArchived(targetChannel);
130
134
  const isTargetChannelPinned = isChannelPinned(targetChannel);
131
- // handle pinning
132
- if (!considerPinnedChannels || lockChannelOrder)
133
- return currentChannels;
134
135
  const newChannels = [...currentChannels];
135
136
  if (targetChannelExistsWithinList) {
136
137
  newChannels.splice(targetChannelIndex, 1);
@@ -8,8 +8,6 @@ import type { StreamMessage } from '../../context/ChannelStateContext';
8
8
  import type { TranslationContextValue } from '../../context/TranslationContext';
9
9
  import type { DefaultStreamChatGenerics } from '../../types/types';
10
10
  export type ChannelPreviewUIComponentProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = ChannelPreviewProps<StreamChatGenerics> & {
11
- /** If the component's channel is the active (selected) Channel */
12
- active?: boolean;
13
11
  /** Image of Channel to display */
14
12
  displayImage?: string;
15
13
  /** Title of Channel to display */
@@ -30,6 +28,8 @@ export type ChannelPreviewUIComponentProps<StreamChatGenerics extends DefaultStr
30
28
  export type ChannelPreviewProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
31
29
  /** Comes from either the `channelRenderFilterFn` or `usePaginatedChannels` call from [ChannelList](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelList/ChannelList.tsx) */
32
30
  channel: Channel<StreamChatGenerics>;
31
+ /** If the component's channel is the active (selected) Channel */
32
+ active?: boolean;
33
33
  /** Current selected channel object */
34
34
  activeChannel?: Channel<StreamChatGenerics>;
35
35
  /** UI component to display an avatar, defaults to [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) component and accepts the same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */
@@ -8,7 +8,7 @@ import { useChatContext } from '../../context/ChatContext';
8
8
  import { useTranslationContext } from '../../context/TranslationContext';
9
9
  import { useMessageDeliveryStatus, } from './hooks/useMessageDeliveryStatus';
10
10
  export const ChannelPreview = (props) => {
11
- const { channel, channelUpdateCount, getLatestMessagePreview = defaultGetLatestMessagePreview, Preview = ChannelPreviewMessenger, } = props;
11
+ const { active, channel, channelUpdateCount, getLatestMessagePreview = defaultGetLatestMessagePreview, Preview = ChannelPreviewMessenger, } = props;
12
12
  const { channel: activeChannel, client, isMessageAIGenerated, setActiveChannel, } = useChatContext('ChannelPreview');
13
13
  const { t, userLanguage } = useTranslationContext('ChannelPreview');
14
14
  const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
@@ -20,7 +20,7 @@ export const ChannelPreview = (props) => {
20
20
  channel,
21
21
  lastMessage,
22
22
  });
23
- const isActive = activeChannel?.cid === channel.cid;
23
+ const isActive = typeof active === 'undefined' ? activeChannel?.cid === channel.cid : active;
24
24
  const { muted } = useIsChannelMuted(channel);
25
25
  useEffect(() => {
26
26
  const handleEvent = (event) => {
@@ -31,8 +31,7 @@ export const ChannelPreview = (props) => {
31
31
  };
32
32
  client.on('notification.mark_read', handleEvent);
33
33
  return () => client.off('notification.mark_read', handleEvent);
34
- // eslint-disable-next-line react-hooks/exhaustive-deps
35
- }, []);
34
+ }, [channel, client]);
36
35
  useEffect(() => {
37
36
  const handleEvent = (event) => {
38
37
  if (channel.cid !== event.cid)
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
- import type { DefaultStreamChatGenerics } from '../../types/types';
3
2
  import type { ChannelPreviewUIComponentProps } from './ChannelPreview';
3
+ import type { DefaultStreamChatGenerics } from '../../types/types';
4
4
  /**
5
5
  * Used as preview component for channel item in [ChannelList](#channellist) component.
6
6
  * Its best suited for messenger type chat.
@@ -33,25 +33,24 @@ export const SearchBar = (props) => {
33
33
  useEffect(() => {
34
34
  if (!props.inputRef.current)
35
35
  return;
36
+ const input = props.inputRef.current;
36
37
  const handleFocus = () => {
37
38
  activateSearch();
38
39
  };
39
40
  const handleBlur = (e) => {
40
41
  e.stopPropagation(); // handle blur/focus state with React state
41
42
  };
42
- props.inputRef.current.addEventListener('focus', handleFocus);
43
- props.inputRef.current.addEventListener('blur', handleBlur);
43
+ input.addEventListener('focus', handleFocus);
44
+ input.addEventListener('blur', handleBlur);
44
45
  return () => {
45
- props.inputRef.current?.removeEventListener('focus', handleFocus);
46
- props.inputRef.current?.addEventListener('blur', handleBlur);
46
+ input.removeEventListener('focus', handleFocus);
47
+ input.removeEventListener('blur', handleBlur);
47
48
  };
48
- // eslint-disable-next-line react-hooks/exhaustive-deps
49
- }, []);
49
+ }, [activateSearch, props.inputRef]);
50
50
  const handleClearClick = useCallback(() => {
51
51
  exitSearch();
52
52
  inputProps.inputRef.current?.focus();
53
- // eslint-disable-next-line react-hooks/exhaustive-deps
54
- }, []);
53
+ }, [exitSearch, inputProps.inputRef]);
55
54
  const closeAppMenu = useCallback(() => setMenuIsOpen(false), []);
56
55
  return (React.createElement("div", { className: 'str-chat__channel-search-bar', "data-testid": 'search-bar', ref: searchBarRef },
57
56
  inputIsFocused ? (React.createElement(SearchBarButton, { className: 'str-chat__channel-search-bar-button--exit-search', onClick: exitSearch },
@@ -60,14 +60,15 @@ export const SearchResults = (props) => {
60
60
  }
61
61
  if (event.key === 'Enter') {
62
62
  event.preventDefault();
63
- if (focusedResult !== undefined) {
64
- selectResult(results[focusedResult]);
65
- return setFocusedResult(undefined);
66
- }
63
+ setFocusedResult((prevFocused) => {
64
+ if (typeof prevFocused !== 'undefined') {
65
+ selectResult(results[prevFocused]);
66
+ return undefined;
67
+ }
68
+ return prevFocused;
69
+ });
67
70
  }
68
- },
69
- // eslint-disable-next-line react-hooks/exhaustive-deps
70
- [focusedResult]);
71
+ }, [results, selectResult]);
71
72
  useEffect(() => {
72
73
  document.addEventListener('keydown', handleKeyDown, false);
73
74
  return () => document.removeEventListener('keydown', handleKeyDown);
@@ -1,10 +1,11 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
- import { CustomClasses } from '../../context/ChatContext';
3
2
  import type { StreamChat } from 'stream-chat';
3
+ import { SearchController } from 'stream-chat';
4
+ import { CustomClasses } from '../../context/ChatContext';
5
+ import type { MessageContextValue } from '../../context';
4
6
  import type { SupportedTranslations } from '../../i18n/types';
5
7
  import type { Streami18n } from '../../i18n/Streami18n';
6
8
  import type { DefaultStreamChatGenerics } from '../../types/types';
7
- import type { MessageContextValue } from '../../context';
8
9
  export type ChatProps<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics> = {
9
10
  /** The StreamChat client object */
10
11
  client: StreamChat<StreamChatGenerics>;
@@ -16,6 +17,8 @@ export type ChatProps<StreamChatGenerics extends DefaultStreamChatGenerics = Def
16
17
  i18nInstance?: Streami18n;
17
18
  /** Initial status of mobile navigation */
18
19
  initialNavOpen?: boolean;
20
+ /** Instance of SearchController class that allows to control all the search operations. */
21
+ searchController?: SearchController<StreamChatGenerics>;
19
22
  /** Used for injecting className/s to the Channel and ChannelList components */
20
23
  theme?: string;
21
24
  /**
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
+ import { ChannelSearchSource, MessageSearchSource, SearchController, UserSearchSource, } from 'stream-chat';
2
3
  import { useChat } from './hooks/useChat';
3
4
  import { useCreateChatContext } from './hooks/useCreateChatContext';
4
5
  import { useChannelsQueryState } from './hooks/useChannelsQueryState';
@@ -9,9 +10,17 @@ import { TranslationProvider } from '../../context/TranslationContext';
9
10
  * as it provides the ChatContext.
10
11
  */
11
12
  export const Chat = (props) => {
12
- const { children, client, customClasses, defaultLanguage, i18nInstance, initialNavOpen = true, isMessageAIGenerated, theme = 'messaging light', useImageFlagEmojisOnWindows = false, } = props;
13
+ const { children, client, customClasses, defaultLanguage, i18nInstance, initialNavOpen = true, isMessageAIGenerated, searchController: customChannelSearchController, theme = 'messaging light', useImageFlagEmojisOnWindows = false, } = props;
13
14
  const { channel, closeMobileNav, getAppSettings, latestMessageDatesByChannels, mutes, navOpen, openMobileNav, setActiveChannel, translators, } = useChat({ client, defaultLanguage, i18nInstance, initialNavOpen });
14
15
  const channelsQueryState = useChannelsQueryState();
16
+ const searchController = useMemo(() => customChannelSearchController ??
17
+ new SearchController({
18
+ sources: [
19
+ new ChannelSearchSource(client),
20
+ new UserSearchSource(client),
21
+ new MessageSearchSource(client),
22
+ ],
23
+ }), [client, customChannelSearchController]);
15
24
  const chatContextValue = useCreateChatContext({
16
25
  channel,
17
26
  channelsQueryState,
@@ -24,6 +33,7 @@ export const Chat = (props) => {
24
33
  mutes,
25
34
  navOpen,
26
35
  openMobileNav,
36
+ searchController,
27
37
  setActiveChannel,
28
38
  theme,
29
39
  useImageFlagEmojisOnWindows,
@@ -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.11.0-${userAgent}`);
31
+ client.setUserAgent(`stream-chat-react-12.12.0-${userAgent}`);
32
32
  }
33
33
  client.threads.registerSubscriptions();
34
34
  client.polls.registerSubscriptions();
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  export const useCreateChatContext = (value) => {
3
- const { channel, channelsQueryState, client, closeMobileNav, customClasses, getAppSettings, isMessageAIGenerated, latestMessageDatesByChannels, mutes, navOpen, openMobileNav, setActiveChannel, theme, useImageFlagEmojisOnWindows, } = value;
3
+ const { channel, channelsQueryState, client, closeMobileNav, customClasses, getAppSettings, isMessageAIGenerated, latestMessageDatesByChannels, mutes, navOpen, openMobileNav, searchController, setActiveChannel, theme, useImageFlagEmojisOnWindows, } = value;
4
4
  const channelCid = channel?.cid;
5
5
  const channelsQueryError = channelsQueryState.error;
6
6
  const channelsQueryInProgress = channelsQueryState.queryInProgress;
@@ -19,6 +19,7 @@ export const useCreateChatContext = (value) => {
19
19
  mutes,
20
20
  navOpen,
21
21
  openMobileNav,
22
+ searchController,
22
23
  setActiveChannel,
23
24
  theme,
24
25
  useImageFlagEmojisOnWindows,
@@ -30,6 +31,7 @@ export const useCreateChatContext = (value) => {
30
31
  channelsQueryInProgress,
31
32
  clientValues,
32
33
  getAppSettings,
34
+ searchController,
33
35
  mutedUsersLength,
34
36
  navOpen,
35
37
  isMessageAIGenerated,
@@ -34,7 +34,7 @@ export type InfiniteScrollProps = PaginatorProps & {
34
34
  /**
35
35
  * This component serves a single purpose - load more items on scroll inside the MessageList component
36
36
  * It is not a general purpose infinite scroll controller, because:
37
- * 1. It is re-rendered whenever isLoading, hasNext, hasPrev changes. This can lead to scrollListener to have stale data.
37
+ * 1. It is re-rendered whenever queryInProgress, hasNext, hasPrev changes. This can lead to scrollListener to have stale data.
38
38
  * 2. It pretends to invoke scrollListener on resize event even though this event is emitted only on window resize. It should
39
39
  * rather use ResizeObserver. But then again, it ResizeObserver would invoke a stale version of scrollListener.
40
40
  *
@@ -13,7 +13,7 @@ const mousewheelListener = (event) => {
13
13
  /**
14
14
  * This component serves a single purpose - load more items on scroll inside the MessageList component
15
15
  * It is not a general purpose infinite scroll controller, because:
16
- * 1. It is re-rendered whenever isLoading, hasNext, hasPrev changes. This can lead to scrollListener to have stale data.
16
+ * 1. It is re-rendered whenever queryInProgress, hasNext, hasPrev changes. This can lead to scrollListener to have stale data.
17
17
  * 2. It pretends to invoke scrollListener on resize event even though this event is emitted only on window resize. It should
18
18
  * rather use ResizeObserver. But then again, it ResizeObserver would invoke a stale version of scrollListener.
19
19
  *
@@ -1,6 +1,7 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
2
  export type InfiniteScrollPaginatorProps = React.ComponentProps<'div'> & {
3
3
  listenToScroll?: (distanceFromBottom: number, distanceFromTop: number, threshold: number) => void;
4
+ loadNextDebounceMs?: number;
4
5
  loadNextOnScrollToBottom?: () => void;
5
6
  loadNextOnScrollToTop?: () => void;
6
7
  /** Offset from when to start the loadNextPage call */
@@ -12,7 +12,7 @@ const mousewheelListener = (event) => {
12
12
  }
13
13
  };
14
14
  export const InfiniteScrollPaginator = (props) => {
15
- const { children, className, listenToScroll, loadNextOnScrollToBottom, loadNextOnScrollToTop, threshold = DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, useCapture = false, ...componentProps } = props;
15
+ const { children, className, listenToScroll, loadNextDebounceMs = 500, loadNextOnScrollToBottom, loadNextOnScrollToTop, threshold = DEFAULT_LOAD_PAGE_SCROLL_THRESHOLD, useCapture = false, ...componentProps } = props;
16
16
  const rootRef = useRef(null);
17
17
  const childRef = useRef(null);
18
18
  const scrollListener = useMemo(() => debounce(() => {
@@ -32,7 +32,13 @@ export const InfiniteScrollPaginator = (props) => {
32
32
  if (distanceFromBottom < Number(threshold)) {
33
33
  loadNextOnScrollToBottom?.();
34
34
  }
35
- }, 500), [listenToScroll, loadNextOnScrollToBottom, loadNextOnScrollToTop, threshold]);
35
+ }, loadNextDebounceMs), [
36
+ listenToScroll,
37
+ loadNextDebounceMs,
38
+ loadNextOnScrollToBottom,
39
+ loadNextOnScrollToTop,
40
+ threshold,
41
+ ]);
36
42
  useEffect(() => {
37
43
  const scrollElement = rootRef.current;
38
44
  if (!scrollElement)
@@ -6,7 +6,12 @@ import { RecordedMediaType } from '../../ReactFileUtilities';
6
6
  import { TranslationContextValue } from '../../../context';
7
7
  import type { LocalVoiceRecordingAttachment } from '../../MessageInput';
8
8
  import type { DefaultStreamChatGenerics } from '../../../types';
9
- export declare const DEFAULT_MEDIA_RECORDER_CONFIG: MediaRecorderConfig;
9
+ export declare const RECORDED_MIME_TYPE_BY_BROWSER: {
10
+ readonly audio: {
11
+ readonly others: "audio/webm";
12
+ readonly safari: "audio/mp4;codecs=mp4a.40.2";
13
+ };
14
+ };
10
15
  export declare const DEFAULT_AUDIO_TRANSCODER_CONFIG: TranscoderConfig;
11
16
  type MediaRecorderConfig = Omit<MediaRecorderOptions, 'mimeType'> & Required<Pick<MediaRecorderOptions, 'mimeType'>>;
12
17
  export type AudioRecorderConfig = {
@@ -7,19 +7,13 @@ import { transcode } from '../transcode';
7
7
  import { resampleWaveformData } from '../../Attachment';
8
8
  import { createFileFromBlobs, getExtensionFromMimeType, getRecordedMediaTypeFromMimeType, } from '../../ReactFileUtilities';
9
9
  import { defaultTranslatorFunction } from '../../../i18n';
10
- import { isSafari } from '../../../utils/browsers';
11
10
  import { mergeDeepUndefined } from '../../../utils/mergeDeep';
12
- const RECORDED_MIME_TYPE_BY_BROWSER = {
11
+ export const RECORDED_MIME_TYPE_BY_BROWSER = {
13
12
  audio: {
14
13
  others: 'audio/webm',
15
14
  safari: 'audio/mp4;codecs=mp4a.40.2',
16
15
  },
17
16
  };
18
- export const DEFAULT_MEDIA_RECORDER_CONFIG = {
19
- mimeType: isSafari()
20
- ? RECORDED_MIME_TYPE_BY_BROWSER.audio.safari
21
- : RECORDED_MIME_TYPE_BY_BROWSER.audio.others,
22
- };
23
17
  export const DEFAULT_AUDIO_TRANSCODER_CONFIG = {
24
18
  sampleRate: 16000,
25
19
  };
@@ -241,7 +235,11 @@ export class MediaRecorderController {
241
235
  };
242
236
  this.t = t || defaultTranslatorFunction;
243
237
  this.amplitudeRecorderConfig = mergeDeepUndefined({ ...config?.amplitudeRecorderConfig }, DEFAULT_AMPLITUDE_RECORDER_CONFIG);
244
- this.mediaRecorderConfig = mergeDeepUndefined({ ...config?.mediaRecorderConfig }, DEFAULT_MEDIA_RECORDER_CONFIG);
238
+ this.mediaRecorderConfig = mergeDeepUndefined({ ...config?.mediaRecorderConfig }, {
239
+ mimeType: MediaRecorder.isTypeSupported('audio/webm')
240
+ ? RECORDED_MIME_TYPE_BY_BROWSER.audio.others
241
+ : RECORDED_MIME_TYPE_BY_BROWSER.audio.safari,
242
+ });
245
243
  this.transcoderConfig = mergeDeepUndefined({ ...config?.transcoderConfig }, DEFAULT_AUDIO_TRANSCODER_CONFIG);
246
244
  const mediaType = getRecordedMediaTypeFromMimeType(this.mediaRecorderConfig.mimeType);
247
245
  if (!mediaType) {
@@ -1,8 +1,8 @@
1
1
  import React, { ComponentType } from 'react';
2
- import { Options } from 'react-markdown';
3
- import type { PluggableList } from 'react-markdown/lib/react-markdown';
4
- import type { UserResponse } from 'stream-chat';
5
2
  import { MentionProps } from './componentRenderers';
3
+ import type { Options } from 'react-markdown/lib';
4
+ import type { UserResponse } from 'stream-chat';
5
+ import type { PluggableList } from 'unified';
6
6
  import type { DefaultStreamChatGenerics } from '../../../types/types';
7
7
  export type RenderTextPluginConfigurator = (defaultPlugins: PluggableList) => PluggableList;
8
8
  export declare const defaultAllowedTagNames: Array<keyof JSX.IntrinsicElements | 'emoji' | 'mention'>;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import ReactMarkdown, { uriTransformer } from 'react-markdown';
2
+ import ReactMarkdown, { defaultUrlTransform } from 'react-markdown';
3
3
  import { find } from 'linkifyjs';
4
4
  import uniqBy from 'lodash.uniqby';
5
5
  import remarkGfm from 'remark-gfm';
@@ -50,7 +50,7 @@ function encodeDecode(url) {
50
50
  return url;
51
51
  }
52
52
  }
53
- const urlTransform = (uri) => uri.startsWith('app://') ? uri : uriTransformer(uri);
53
+ const urlTransform = (uri) => uri.startsWith('app://') ? uri : defaultUrlTransform(uri);
54
54
  const getPluginsForward = (plugins) => plugins;
55
55
  export const markDownRenderers = {
56
56
  a: Anchor,
@@ -118,5 +118,5 @@ export const renderText = (text, mentionedUsers, { allowedTagNames = defaultAllo
118
118
  React.createElement(ReactMarkdown, { allowedElements: allowedTagNames, components: {
119
119
  ...markDownRenderers,
120
120
  ...customMarkDownRenderers,
121
- }, rehypePlugins: getRehypePlugins(rehypePlugins), remarkPlugins: getRemarkPlugins(remarkPlugins), skipHtml: true, transformLinkUri: urlTransform, unwrapDisallowed: true }, newText)));
121
+ }, rehypePlugins: getRehypePlugins(rehypePlugins), remarkPlugins: getRemarkPlugins(remarkPlugins), skipHtml: true, unwrapDisallowed: true, urlTransform: urlTransform }, newText)));
122
122
  };
@@ -1,5 +1,5 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
- import type { AppSettingsAPIResponse, Channel, Mute } from 'stream-chat';
2
+ import type { AppSettingsAPIResponse, Channel, Mute, SearchController } from 'stream-chat';
3
3
  import type { ChatProps } from '../components/Chat/Chat';
4
4
  import type { DefaultStreamChatGenerics, UnknownType } from '../types/types';
5
5
  import type { ChannelsQueryState } from '../components/Chat/hooks/useChannelsQueryState';
@@ -16,6 +16,8 @@ export type ChatContextValue<StreamChatGenerics extends DefaultStreamChatGeneric
16
16
  latestMessageDatesByChannels: Record<ChannelCID, Date>;
17
17
  mutes: Array<Mute<StreamChatGenerics>>;
18
18
  openMobileNav: () => void;
19
+ /** Instance of SearchController class that allows to control all the search operations. */
20
+ searchController: SearchController<StreamChatGenerics>;
19
21
  /**
20
22
  * Sets active channel to be rendered within Channel component.
21
23
  * @param newChannel