From 6f2de4ebd8e8a5b9668b44bb0204ea695c48ff01 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:10:49 +0100 Subject: [PATCH 1/8] fix: change useStateStore to use useSyncExternalStore (#2573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🎯 Goal Changing stores on the fly would keep previously calculated state for a bit before the effect would run to recalculate it - using `useSyncExternalStore` (thank you, @myandrienko) should alleviate this issue. Both `subscribe` and `getSnapshot` functions required by the React hook are wrapped to allow for selector functionality, [`geSnapshot` requires the output to be cached](https://react.dev/reference/react/useSyncExternalStore#parameters) so the wrapper reuses similar cache check mechanism as `subscribeWithSelector` does internally. --- src/store/hooks/useStateStore.ts | 52 ++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/store/hooks/useStateStore.ts b/src/store/hooks/useStateStore.ts index a74bfe9d1f..cc7a326d40 100644 --- a/src/store/hooks/useStateStore.ts +++ b/src/store/hooks/useStateStore.ts @@ -1,7 +1,11 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; import type { StateStore } from 'stream-chat'; +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + export function useStateStore< T extends Record, O extends Readonly | Readonly>, @@ -14,18 +18,48 @@ export function useStateStore< T extends Record, O extends Readonly | Readonly>, >(store: StateStore | undefined, selector: (v: T) => O) { - const [state, setState] = useState(() => { - if (!store) return undefined; - return selector(store.getLatestValue()); - }); + const wrappedSubscription = useCallback( + (onStoreChange: () => void) => { + const unsubscribe = store?.subscribeWithSelector(selector, onStoreChange); + return unsubscribe ?? noop; + }, + [store, selector], + ); + + const wrappedSnapshot = useMemo(() => { + let cachedTuple: [T, O]; + + return () => { + const currentValue = store?.getLatestValue(); + + if (!currentValue) return undefined; - useEffect(() => { - if (!store) return; + // store value hasn't changed, no need to compare individual values + if (cachedTuple && cachedTuple[0] === currentValue) { + return cachedTuple[1]; + } - const unsubscribe = store.subscribeWithSelector(selector, setState); + const newlySelected = selector(currentValue); - return unsubscribe; + // store value changed but selected values wouldn't have to, double-check selected + if (cachedTuple) { + let selectededAreEqualToCached = true; + + for (const key in cachedTuple[1]) { + if (cachedTuple[1][key] === newlySelected[key]) continue; + selectededAreEqualToCached = false; + break; + } + + if (selectededAreEqualToCached) return cachedTuple[1]; + } + + cachedTuple = [currentValue, newlySelected]; + return cachedTuple[1]; + }; }, [store, selector]); + const state = useSyncExternalStore(wrappedSubscription, wrappedSnapshot); + return state; } From a428dc938388947b1f7e51fde3dd579eba76ea2f Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:12:51 +0100 Subject: [PATCH 2/8] feat: allow custom ReactionsListModal (#2632) --- src/components/Channel/Channel.tsx | 3 ++ src/components/Reactions/ReactionsList.tsx | 39 ++++++++++++++----- .../Reactions/ReactionsListModal.tsx | 2 +- .../Reactions/__tests__/ReactionsList.test.js | 29 ++++++++++++-- src/components/Reactions/index.ts | 1 + src/context/ComponentContext.tsx | 3 ++ 6 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 0ef286dec3..f069626d10 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -150,6 +150,7 @@ type ChannelPropsForwardedToComponentContext< | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' + | 'ReactionsListModal' | 'SendButton' | 'StartRecordingAudioButton' | 'ThreadHead' @@ -1340,6 +1341,7 @@ const ChannelInner = < reactionOptions: props.reactionOptions, ReactionSelector: props.ReactionSelector, ReactionsList: props.ReactionsList, + ReactionsListModal: props.ReactionsListModal, SendButton: props.SendButton, StartRecordingAudioButton: props.StartRecordingAudioButton, StopAIGenerationButton: props.StopAIGenerationButton, @@ -1399,6 +1401,7 @@ const ChannelInner = < props.QuotedPoll, props.ReactionSelector, props.ReactionsList, + props.ReactionsListModal, props.SendButton, props.StartRecordingAudioButton, props.ThreadHead, diff --git a/src/components/Reactions/ReactionsList.tsx b/src/components/Reactions/ReactionsList.tsx index 14e2445fc0..9c388a3384 100644 --- a/src/components/Reactions/ReactionsList.tsx +++ b/src/components/Reactions/ReactionsList.tsx @@ -1,9 +1,20 @@ import React, { useState } from 'react'; import clsx from 'clsx'; -import type { ReactionGroupResponse, ReactionResponse } from 'stream-chat'; - +import { + ReactionsListModal as DefaultReactionsListModal, + ReactionsListModalProps, +} from './ReactionsListModal'; import { useProcessReactions } from './hooks/useProcessReactions'; +import { + MessageContextValue, + useComponentContext, + useTranslationContext, +} from '../../context'; + +import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks'; + +import type { ReactionGroupResponse, ReactionResponse, ReactionSort } from 'stream-chat'; import type { DefaultStreamChatGenerics } from '../../types/types'; import type { ReactionOptions } from './reactionOptions'; import type { @@ -11,9 +22,6 @@ import type { ReactionsComparator, ReactionType, } from './types'; -import { ReactionsListModal } from './ReactionsListModal'; -import { MessageContextValue, useTranslationContext } from '../../context'; -import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks'; export type ReactionsListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -44,7 +52,7 @@ export type ReactionsListProps< /** Comparator function to sort the list of reacted users * @deprecated use `reactionDetailsSort` instead */ - sortReactionDetails?: ReactionDetailsComparator; + sortReactionDetails?: ReactionDetailsComparator; /** Comparator function to sort reactions, defaults to chronological order */ sortReactions?: ReactionsComparator; }; @@ -67,6 +75,7 @@ const UnMemoizedReactionsList = < const [selectedReactionType, setSelectedReactionType] = useState | null>(null); const { t } = useTranslationContext('ReactionsList'); + const { ReactionsListModal = DefaultReactionsListModal } = useComponentContext(); const handleReactionButtonClick = (reactionType: string) => { if (totalReactionCount > MAX_MESSAGE_REACTIONS_TO_FETCH) { @@ -126,13 +135,25 @@ const UnMemoizedReactionsList = < {selectedReactionType !== null && ( , + ) => Promise>> + } onClose={() => setSelectedReactionType(null)} - onSelectedReactionTypeChange={setSelectedReactionType} + onSelectedReactionTypeChange={ + setSelectedReactionType as ReactionsListModalProps['onSelectedReactionTypeChange'] + } open={selectedReactionType !== null} reactions={existingReactions} selectedReactionType={selectedReactionType} - sortReactionDetails={sortReactionDetails} + sortReactionDetails={ + sortReactionDetails as ( + a: ReactionResponse, + b: ReactionResponse, + ) => number + } /> )} diff --git a/src/components/Reactions/ReactionsListModal.tsx b/src/components/Reactions/ReactionsListModal.tsx index dfaf91c693..ad218350fb 100644 --- a/src/components/Reactions/ReactionsListModal.tsx +++ b/src/components/Reactions/ReactionsListModal.tsx @@ -11,7 +11,7 @@ import { MessageContextValue, useMessageContext } from '../../context'; import { DefaultStreamChatGenerics } from '../../types/types'; import { ReactionSort } from 'stream-chat'; -type ReactionsListModalProps< +export type ReactionsListModalProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = ModalProps & Partial< diff --git a/src/components/Reactions/__tests__/ReactionsList.test.js b/src/components/Reactions/__tests__/ReactionsList.test.js index cade672e4d..539106988f 100644 --- a/src/components/Reactions/__tests__/ReactionsList.test.js +++ b/src/components/Reactions/__tests__/ReactionsList.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { toHaveNoViolations } from 'jest-axe'; @@ -16,7 +16,11 @@ import { defaultReactionOptions } from '../reactionOptions'; const USER_ID = 'mark'; -const renderComponent = ({ reaction_groups = {}, ...props }) => { +const renderComponent = ({ + reaction_groups = {}, + ReactionsListModal = undefined, + ...props +}) => { const reactions = Object.entries(reaction_groups).flatMap(([type, { count }]) => Array.from({ length: count }, (_, i) => generateReaction({ type, user: { id: `${USER_ID}-${i}` } }), @@ -24,7 +28,9 @@ const renderComponent = ({ reaction_groups = {}, ...props }) => { ); return render( - + { // disable warnings (unreachable context) jest.spyOn(console, 'warn').mockImplementation(null); + it('renders custom ReactionsListModal', async () => { + const CUSTOM_MODAL_TEST_ID = 'custom-reaction-list-modal'; + const ReactionsListModal = () =>
; + renderComponent({ + reaction_groups: { + haha: { count: 2 }, + love: { count: 5 }, + }, + ReactionsListModal, + }); + + await act(() => { + fireEvent.click(screen.getByTestId('reactions-list-button-haha')); + }); + expect(screen.getByTestId(CUSTOM_MODAL_TEST_ID)).toBeInTheDocument(); + }); + it('should render the total reaction count', async () => { const { container, getByText } = renderComponent({ reaction_groups: { diff --git a/src/components/Reactions/index.ts b/src/components/Reactions/index.ts index 891aa1addd..d17fd6d927 100644 --- a/src/components/Reactions/index.ts +++ b/src/components/Reactions/index.ts @@ -1,5 +1,6 @@ export * from './ReactionSelector'; export * from './ReactionsList'; +export * from './ReactionsListModal'; export * from './SimpleReactionsList'; export * from './SpriteImage'; export * from './StreamEmoji'; diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 02f115600c..3dec7d47fd 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -34,6 +34,7 @@ import { QuotedMessagePreviewProps, ReactionOptions, ReactionSelectorProps, + ReactionsListModalProps, ReactionsListProps, RecordingPermissionDeniedNotificationProps, SendButtonProps, @@ -176,6 +177,8 @@ export type ComponentContextValue< >; /** Custom UI component to display the list of reactions on a message, defaults to and accepts same props as: [ReactionsList](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/ReactionsList.tsx) */ ReactionsList?: React.ComponentType>; + /** Custom UI component to display the reactions modal, defaults to and accepts same props as: [ReactionsListModal](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/ReactionsListModal.tsx) */ + ReactionsListModal?: React.ComponentType>; RecordingPermissionDeniedNotification?: React.ComponentType; /** Custom UI component for send button, defaults to and accepts same props as: [SendButton](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/icons.tsx) */ SendButton?: React.ComponentType>; From 2d2e2e56e6aff92a00d3dd2e6da8c5f7b64698e1 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:26:52 +0100 Subject: [PATCH 3/8] fix: make `channel.visible` respect archived and pinned channels (#2633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🎯 Goal References: https://github.com/GetStream/stream-chat-react-native/pull/2925 --- .../ChannelList/hooks/useChannelListShape.ts | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/components/ChannelList/hooks/useChannelListShape.ts b/src/components/ChannelList/hooks/useChannelListShape.ts index 8ecfcb5d0e..bcb2b43ad5 100644 --- a/src/components/ChannelList/hooks/useChannelListShape.ts +++ b/src/components/ChannelList/hooks/useChannelListShape.ts @@ -2,7 +2,6 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react'; import { Channel, Event, ExtendableGenerics } from 'stream-chat'; -import uniqBy from 'lodash.uniqby'; import { extractSortValue, @@ -68,7 +67,9 @@ type HandleChannelHiddenParameters = BaseParamet RepeatedParameters; type HandleChannelVisibleParameters = - BaseParameters & RepeatedParameters; + BaseParameters & + RepeatedParameters & + Required, 'sort' | 'filters'>>; type HandleChannelTruncatedParameters = BaseParameters & RepeatedParameters; @@ -194,7 +195,6 @@ export const useChannelListShapeDefaults = () => moveChannelUpwards({ channels, channelToMove: channel, - channelToMoveIndexWithinChannels: -1, sort, }), ); @@ -239,7 +239,6 @@ export const useChannelListShapeDefaults = () => moveChannelUpwards({ channels, channelToMove: channel, - channelToMoveIndexWithinChannels: -1, sort, }), ); @@ -353,20 +352,36 @@ export const useChannelListShapeDefaults = () => async ({ customHandler, event, + filters, setChannels, + sort, }: HandleChannelVisibleParameters) => { if (typeof customHandler === 'function') { return customHandler(setChannels, event); } - if (event.type && event.channel_type && event.channel_id) { - const channel = await getChannel({ - client, - id: event.channel_id, - type: event.channel_type, - }); - setChannels((channels) => uniqBy([channel, ...channels], 'cid')); + if (!event.channel) { + return; + } + + const channel = await getChannel({ + client, + id: event.channel.id, + type: event.channel.type, + }); + + const considerArchivedChannels = shouldConsiderArchivedChannels(filters); + if (isChannelArchived(channel) && considerArchivedChannels && !filters.archived) { + return; } + + setChannels((channels) => + moveChannelUpwards({ + channels, + channelToMove: channel, + sort, + }), + ); }, [client], ); @@ -586,7 +601,9 @@ export const usePrepareShapeHandlers = ({ defaults.handleChannelVisible({ customHandler: onChannelVisible, event, + filters, setChannels, + sort, }); break; case 'channel.truncated': From 8b7cfba7140588f5fd1d727b48b7e6c7da5d99fe Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:15:23 +0100 Subject: [PATCH 4/8] chore(deps): upgrade @stream-io/stream-chat-css to version 5.7.0 (#2636) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index af1a77c06f..08c701ddf5 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,7 @@ "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", "@stream-io/rollup-plugin-node-builtins": "^2.1.5", - "@stream-io/stream-chat-css": "^5.6.0", + "@stream-io/stream-chat-css": "^5.7.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", diff --git a/yarn.lock b/yarn.lock index a4e499998e..1bd91e5c62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2368,10 +2368,10 @@ crypto-browserify "^3.11.0" process-es6 "^0.11.2" -"@stream-io/stream-chat-css@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-5.6.0.tgz#0eb6c29f4c77772d2418e37983d0efbf8a94ce69" - integrity sha512-IyGnYsZYK7U/NAB9U+agXkSx9hRifubJa1NHhsy0EO0p5E2/wfdQTmPiy79Ba4RbPbzHEG5PPWNBdE3Ovpflsw== +"@stream-io/stream-chat-css@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-5.7.0.tgz#9626f35ae4eb5320bec90ba27a343c00e5bbd3e7" + integrity sha512-3CtbS5BV0PfW1kTDJtj1oSoPEreINu2Q9cJEEXUmRguOLb6LMjS3OsSnZq78RYHdECNfta3I2M8JxdFlRTEKSA== "@stream-io/transliterate@^1.5.5": version "1.5.5" From ed0906cb5e4f9853bbd3cd98b97f00500b979a92 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 7 Feb 2025 12:19:00 +0000 Subject: [PATCH 5/8] chore(release): 12.11.0 [skip ci] ## [12.11.0](https://github.com/GetStream/stream-chat-react/compare/v12.10.0...v12.11.0) (2025-02-07) ### Bug Fixes * change useStateStore to use useSyncExternalStore ([#2573](https://github.com/GetStream/stream-chat-react/issues/2573)) ([6f2de4e](https://github.com/GetStream/stream-chat-react/commit/6f2de4ebd8e8a5b9668b44bb0204ea695c48ff01)) * make `channel.visible` respect archived and pinned channels ([#2633](https://github.com/GetStream/stream-chat-react/issues/2633)) ([2d2e2e5](https://github.com/GetStream/stream-chat-react/commit/2d2e2e56e6aff92a00d3dd2e6da8c5f7b64698e1)) ### Features * allow custom ReactionsListModal ([#2632](https://github.com/GetStream/stream-chat-react/issues/2632)) ([a428dc9](https://github.com/GetStream/stream-chat-react/commit/a428dc938388947b1f7e51fde3dd579eba76ea2f)) * allow to search for channels only ([#2625](https://github.com/GetStream/stream-chat-react/issues/2625)) ([a4d6d83](https://github.com/GetStream/stream-chat-react/commit/a4d6d83cf7c63f3284f0e0c3d0689f64ae725323)) ### Chores * **deps:** upgrade @stream-io/stream-chat-css to version 5.7.0 ([#2636](https://github.com/GetStream/stream-chat-react/issues/2636)) ([8b7cfba](https://github.com/GetStream/stream-chat-react/commit/8b7cfba7140588f5fd1d727b48b7e6c7da5d99fe)) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 486657b981..baccaa6317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## [12.11.0](https://github.com/GetStream/stream-chat-react/compare/v12.10.0...v12.11.0) (2025-02-07) + + +### Bug Fixes + +* change useStateStore to use useSyncExternalStore ([#2573](https://github.com/GetStream/stream-chat-react/issues/2573)) ([6f2de4e](https://github.com/GetStream/stream-chat-react/commit/6f2de4ebd8e8a5b9668b44bb0204ea695c48ff01)) +* make `channel.visible` respect archived and pinned channels ([#2633](https://github.com/GetStream/stream-chat-react/issues/2633)) ([2d2e2e5](https://github.com/GetStream/stream-chat-react/commit/2d2e2e56e6aff92a00d3dd2e6da8c5f7b64698e1)) + + +### Features + +* allow custom ReactionsListModal ([#2632](https://github.com/GetStream/stream-chat-react/issues/2632)) ([a428dc9](https://github.com/GetStream/stream-chat-react/commit/a428dc938388947b1f7e51fde3dd579eba76ea2f)) +* allow to search for channels only ([#2625](https://github.com/GetStream/stream-chat-react/issues/2625)) ([a4d6d83](https://github.com/GetStream/stream-chat-react/commit/a4d6d83cf7c63f3284f0e0c3d0689f64ae725323)) + + +### Chores + +* **deps:** upgrade @stream-io/stream-chat-css to version 5.7.0 ([#2636](https://github.com/GetStream/stream-chat-react/issues/2636)) ([8b7cfba](https://github.com/GetStream/stream-chat-react/commit/8b7cfba7140588f5fd1d727b48b7e6c7da5d99fe)) + ## [12.10.0](https://github.com/GetStream/stream-chat-react/compare/v12.9.0...v12.10.0) (2025-01-28) From ae66de8d3c5ed78fbe5aa5388ebf8a1b8e90a9e9 Mon Sep 17 00:00:00 2001 From: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:46:59 +0100 Subject: [PATCH 6/8] fix(handleMemberUpdated): consider both pinned and archived channels (#2638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🎯 Goal This PR solves an issue where if `pinned_at` is missing from the `sort`, the archived channels are not being handled properly during application runtime. --- src/components/ChannelList/hooks/useChannelListShape.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ChannelList/hooks/useChannelListShape.ts b/src/components/ChannelList/hooks/useChannelListShape.ts index bcb2b43ad5..b9208f6a5d 100644 --- a/src/components/ChannelList/hooks/useChannelListShape.ts +++ b/src/components/ChannelList/hooks/useChannelListShape.ts @@ -285,6 +285,11 @@ export const useChannelListShapeDefaults = () => const considerPinnedChannels = shouldConsiderPinnedChannels(sort); const considerArchivedChannels = shouldConsiderArchivedChannels(filters); + // `pinned_at` nor `archived` properties are set or channel list order is locked, return early + if ((!considerPinnedChannels && !considerArchivedChannels) || lockChannelOrder) { + return; + } + const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' }); setChannels((currentChannels) => { @@ -296,9 +301,6 @@ export const useChannelListShapeDefaults = () => const isTargetChannelArchived = isChannelArchived(targetChannel); const isTargetChannelPinned = isChannelPinned(targetChannel); - // handle pinning - if (!considerPinnedChannels || lockChannelOrder) return currentChannels; - const newChannels = [...currentChannels]; if (targetChannelExistsWithinList) { From 881e4b8aa6faf3650095fee6a63d45a1f36022d1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 11 Feb 2025 07:49:41 +0000 Subject: [PATCH 7/8] chore(release): 12.11.1 [skip ci] ## [12.11.1](https://github.com/GetStream/stream-chat-react/compare/v12.11.0...v12.11.1) (2025-02-11) ### Bug Fixes * **handleMemberUpdated:** consider both pinned and archived channels ([#2638](https://github.com/GetStream/stream-chat-react/issues/2638)) ([ae66de8](https://github.com/GetStream/stream-chat-react/commit/ae66de8d3c5ed78fbe5aa5388ebf8a1b8e90a9e9)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index baccaa6317..b36b1fa4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [12.11.1](https://github.com/GetStream/stream-chat-react/compare/v12.11.0...v12.11.1) (2025-02-11) + + +### Bug Fixes + +* **handleMemberUpdated:** consider both pinned and archived channels ([#2638](https://github.com/GetStream/stream-chat-react/issues/2638)) ([ae66de8](https://github.com/GetStream/stream-chat-react/commit/ae66de8d3c5ed78fbe5aa5388ebf8a1b8e90a9e9)) + ## [12.11.0](https://github.com/GetStream/stream-chat-react/compare/v12.10.0...v12.11.0) (2025-02-07) From 1c445de211e8ed85371cf895f89e96f961e4c377 Mon Sep 17 00:00:00 2001 From: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:39:55 +0100 Subject: [PATCH 8/8] fix: determine audio recording format with MediaRecorder.isTypeSupported not userAgent (#2639) --- .../classes/MediaRecorderController.ts | 15 +++++------ .../__tests__/MediaRecorderController.test.js | 25 ++++++++++++++++--- .../hooks/__tests__/useMediaRecorder.test.js | 8 +++--- src/mock-builders/browser/MediaRecorder.js | 9 ++++--- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/components/MediaRecorder/classes/MediaRecorderController.ts b/src/components/MediaRecorder/classes/MediaRecorderController.ts index 7b7c804db9..684e5687d6 100644 --- a/src/components/MediaRecorder/classes/MediaRecorderController.ts +++ b/src/components/MediaRecorder/classes/MediaRecorderController.ts @@ -17,25 +17,18 @@ import { } from '../../ReactFileUtilities'; import { TranslationContextValue } from '../../../context'; import { defaultTranslatorFunction } from '../../../i18n'; -import { isSafari } from '../../../utils/browsers'; import { mergeDeepUndefined } from '../../../utils/mergeDeep'; import type { LocalVoiceRecordingAttachment } from '../../MessageInput'; import type { DefaultStreamChatGenerics } from '../../../types'; -const RECORDED_MIME_TYPE_BY_BROWSER = { +export const RECORDED_MIME_TYPE_BY_BROWSER = { audio: { others: 'audio/webm', safari: 'audio/mp4;codecs=mp4a.40.2', }, } as const; -export const DEFAULT_MEDIA_RECORDER_CONFIG: MediaRecorderConfig = { - mimeType: isSafari() - ? RECORDED_MIME_TYPE_BY_BROWSER.audio.safari - : RECORDED_MIME_TYPE_BY_BROWSER.audio.others, -} as const; - export const DEFAULT_AUDIO_TRANSCODER_CONFIG: TranscoderConfig = { sampleRate: 16000, } as const; @@ -120,7 +113,11 @@ export class MediaRecorderController< this.mediaRecorderConfig = mergeDeepUndefined( { ...config?.mediaRecorderConfig }, - DEFAULT_MEDIA_RECORDER_CONFIG, + { + mimeType: MediaRecorder.isTypeSupported('audio/webm') + ? RECORDED_MIME_TYPE_BY_BROWSER.audio.others + : RECORDED_MIME_TYPE_BY_BROWSER.audio.safari, + }, ); this.transcoderConfig = mergeDeepUndefined( diff --git a/src/components/MediaRecorder/classes/__tests__/MediaRecorderController.test.js b/src/components/MediaRecorder/classes/__tests__/MediaRecorderController.test.js index 7d20432acd..27cafb2ca2 100644 --- a/src/components/MediaRecorder/classes/__tests__/MediaRecorderController.test.js +++ b/src/components/MediaRecorder/classes/__tests__/MediaRecorderController.test.js @@ -3,9 +3,9 @@ import * as transcoder from '../../transcode'; import * as wavTranscoder from '../../transcode/wav'; import { DEFAULT_AUDIO_TRANSCODER_CONFIG, - DEFAULT_MEDIA_RECORDER_CONFIG, MediaRecorderController, MediaRecordingState, + RECORDED_MIME_TYPE_BY_BROWSER, RecordingAttachmentType, } from '../MediaRecorderController'; import { @@ -92,10 +92,27 @@ describe('MediaRecorderController', () => { }); afterEach(jest.clearAllMocks); - it('provides defaults on initiation', () => { + it('provides defaults on initiation (non-Safari)', () => { const controller = new MediaRecorderController(); expect(controller.mediaRecorderConfig).toStrictEqual( - expect.objectContaining(DEFAULT_MEDIA_RECORDER_CONFIG), + expect.objectContaining({ mimeType: RECORDED_MIME_TYPE_BY_BROWSER.audio.others }), + ); + expect(controller.transcoderConfig).toStrictEqual( + expect.objectContaining(DEFAULT_AUDIO_TRANSCODER_CONFIG), + ); + expect(controller.amplitudeRecorderConfig).toStrictEqual( + expect.objectContaining(DEFAULT_AMPLITUDE_RECORDER_CONFIG), + ); + expect(controller.t).toStrictEqual(defaultTranslatorFunction); + expect(controller.mediaType).toStrictEqual('audio'); + expect(controller.customGenerateRecordingTitle).toBeUndefined(); + }); + + it('provides defaults on initiation (Safari)', () => { + MediaRecorder.isTypeSupported.mockReturnValueOnce(false); + const controller = new MediaRecorderController(); + expect(controller.mediaRecorderConfig).toStrictEqual( + expect.objectContaining({ mimeType: RECORDED_MIME_TYPE_BY_BROWSER.audio.safari }), ); expect(controller.transcoderConfig).toStrictEqual( expect.objectContaining(DEFAULT_AUDIO_TRANSCODER_CONFIG), @@ -151,7 +168,7 @@ describe('MediaRecorderController', () => { const controller = new MediaRecorderController({ generateRecordingTitle }); expect(controller.customGenerateRecordingTitle).toStrictEqual(generateRecordingTitle); expect(controller.mediaRecorderConfig).toStrictEqual( - expect.objectContaining(DEFAULT_MEDIA_RECORDER_CONFIG), + expect.objectContaining({ mimeType: RECORDED_MIME_TYPE_BY_BROWSER.audio.others }), ); expect(controller.transcoderConfig).toStrictEqual( expect.objectContaining(DEFAULT_AUDIO_TRANSCODER_CONFIG), diff --git a/src/components/MediaRecorder/hooks/__tests__/useMediaRecorder.test.js b/src/components/MediaRecorder/hooks/__tests__/useMediaRecorder.test.js index 4cfbfba97b..b68849e3a0 100644 --- a/src/components/MediaRecorder/hooks/__tests__/useMediaRecorder.test.js +++ b/src/components/MediaRecorder/hooks/__tests__/useMediaRecorder.test.js @@ -2,12 +2,14 @@ import { TranslationProvider } from '../../../../context'; import { renderHook } from '@testing-library/react'; import React from 'react'; import { useMediaRecorder } from '../useMediaRecorder'; -import { EventEmitterMock } from '../../../../mock-builders/browser'; +import { EventEmitterMock, MediaRecorderMock } from '../../../../mock-builders/browser'; import { act } from '@testing-library/react'; import { DEFAULT_AMPLITUDE_RECORDER_CONFIG } from '../../classes/AmplitudeRecorder'; import { DEFAULT_AUDIO_TRANSCODER_CONFIG } from '../../classes'; import { generateVoiceRecordingAttachment } from '../../../../mock-builders'; +window.MediaRecorder = MediaRecorderMock; + const handleSubmit = jest.fn(); const uploadAttachment = jest.fn(); @@ -27,8 +29,8 @@ const render = async (params = {}) => { {children} ); let result; - await act(() => { - result = renderHook(() => useMediaRecorder({ enabled: true, ...params }), { + await act(async () => { + result = await renderHook(() => useMediaRecorder({ enabled: true, ...params }), { wrapper, }); }); diff --git a/src/mock-builders/browser/MediaRecorder.js b/src/mock-builders/browser/MediaRecorder.js index 95906b3c75..55167da41b 100644 --- a/src/mock-builders/browser/MediaRecorder.js +++ b/src/mock-builders/browser/MediaRecorder.js @@ -3,10 +3,11 @@ import { EventEmitterMock } from './EventEmitter'; export class MediaRecorderMock extends EventEmitterMock { constructor() { super(); + this.start = jest.fn(); + this.pause = jest.fn(); + this.resume = jest.fn(); + this.stop = jest.fn(); } - start = jest.fn(); - pause = jest.fn(); - resume = jest.fn(); - stop = jest.fn(); + static isTypeSupported = jest.fn().mockReturnValue(true); }