From 674ebf01e0f121f101297f4dfc56fb764959d2ab Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Mon, 22 Dec 2025 11:07:49 +0900 Subject: [PATCH 1/4] enhancement: update useRef types for React 19 compatibility --- package.json | 2 +- .../useGroupChannelListWithCollection.ts | 2 +- .../useGroupChannelListWithQuery.ts | 2 +- .../useGroupChannelMessagesWithCollection.ts | 2 +- .../useGroupChannelMessagesWithQuery.ts | 2 +- .../useOpenChannelListWithQuery.ts | 2 +- .../useOpenChannelMessagesWithQuery.ts | 2 +- .../src/common/useUserList.ts | 2 +- .../src/handler/useConnectionHandler.ts | 2 +- .../components/ImageWithPlaceholder/index.tsx | 2 +- .../src/ui/Avatar/AvatarGroup.tsx | 13 +++- .../src/ui/Avatar/AvatarStack.tsx | 9 ++- .../src/ui/Dialog/index.tsx | 6 +- .../components/ChannelMessageList/index.tsx | 6 +- .../ChannelThreadMessageList/index.tsx | 6 +- .../FileViewer/FileViewerContent.tsx | 2 +- .../ReactionUserListBottomSheet.tsx | 2 +- .../component/GroupChannelMessageList.tsx | 4 +- .../createGroupChannelMembersFragment.tsx | 2 +- .../createOpenChannelParticipantsFragment.tsx | 2 +- .../src/hooks/useMentionTextInput.ts | 2 +- .../src/hooks/usePushTokenRegistration.ts | 2 +- .../src/hooks/useVoiceMessageInput.ts | 2 +- packages/uikit-utils/src/hooks/index.ts | 2 +- .../uikit-utils/src/hooks/react-native.ts | 2 +- yarn.lock | 70 ++++++++++++++----- 26 files changed, 100 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 793bc821d..efeaad022 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,6 @@ }, "resolutions": { "@sendbird/chat": "4.20.2", - "@types/react": "^18" + "@types/react": "^19.1.1" } } diff --git a/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithCollection.ts b/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithCollection.ts index 11f75557a..b52ef6dd1 100644 --- a/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithCollection.ts +++ b/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithCollection.ts @@ -33,7 +33,7 @@ export const useGroupChannelListWithCollection: UseGroupChannelList = (sdk, user const handlerId = useUniqHandlerId('useGroupChannelListWithCollection'); const { deliveryReceiptEnabled } = useAppFeatures(sdk); - const collectionRef = useRef(); + const collectionRef = useRef(undefined); const { loading, groupChannels, refreshing, appendChannels, deleteChannels, updateRefreshing, updateLoading } = useGroupChannelListReducer(); diff --git a/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithQuery.ts index 85a8c2562..a5d48e653 100644 --- a/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useGroupChannelList/useGroupChannelListWithQuery.ts @@ -28,7 +28,7 @@ const createGroupChannelListQuery = ( * */ export const useGroupChannelListWithQuery: UseGroupChannelList = (sdk, userId, options) => { const { deliveryReceiptEnabled } = useAppFeatures(sdk); - const queryRef = useRef(); + const queryRef = useRef(undefined); const handlerId = useUniqHandlerId('useGroupChannelListWithQuery'); const { diff --git a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts index dd1f0cacb..534cb2688 100644 --- a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts +++ b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts @@ -54,7 +54,7 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s const initialLimit = shouldUseSearchLimit(initialStartingPoint) ? MESSAGE_LIMIT.SEARCH : MESSAGE_LIMIT.DEFAULT; const forceUpdate = useForceUpdate(); - const collectionRef = useRef(); + const collectionRef = useRef(undefined); const collectionInitializedRef = useRef(false); const handlerId = useUniqHandlerId('useGroupChannelMessagesWithCollection'); diff --git a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts index 7bd783a2b..282f12df3 100644 --- a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts @@ -35,7 +35,7 @@ const createMessageQuery = (channel: SendbirdGroupChannel, options?: UseGroupCha * @deprecated This hook is deprecated and will be replaced by the '@sendbird/uikit-tools' package. * */ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, channel, userId, options) => { - const queryRef = useRef(); + const queryRef = useRef(undefined); const handlerId = useUniqHandlerId('useGroupChannelMessagesWithQuery'); const forceUpdate = useForceUpdate(); diff --git a/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts index 61c862bfe..2f1634db3 100644 --- a/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts @@ -14,7 +14,7 @@ const createOpenChannelListQuery = (sdk: SendbirdChatSDK, queryCreator: UseOpenC }; export const useOpenChannelListWithQuery: UseOpenChannelList = (sdk, userId, options) => { - const queryRef = useRef(); + const queryRef = useRef(undefined); const handlerId = useUniqHandlerId('useOpenChannelListWithQuery'); const { diff --git a/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts index 53c0b4c7f..5e836e4b8 100644 --- a/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts @@ -32,7 +32,7 @@ const createMessageQuery = (channel: SendbirdOpenChannel, creator?: UseOpenChann }; export const useOpenChannelMessagesWithQuery: UseOpenChannelMessages = (sdk, channel, userId, options) => { - const queryRef = useRef(); + const queryRef = useRef(undefined); const forceUpdate = useForceUpdate(); const handlerId = useUniqHandlerId('useOpenChannelMessagesWithQuery'); diff --git a/packages/uikit-chat-hooks/src/common/useUserList.ts b/packages/uikit-chat-hooks/src/common/useUserList.ts index 0e985708d..e526ce185 100644 --- a/packages/uikit-chat-hooks/src/common/useUserList.ts +++ b/packages/uikit-chat-hooks/src/common/useUserList.ts @@ -46,7 +46,7 @@ export const useUserList = < sdk: SendbirdChatSDK, options?: UseUserListOptions, ): UseUserListReturn => { - const query = useRef>(); + const query = useRef | undefined>(undefined); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); diff --git a/packages/uikit-chat-hooks/src/handler/useConnectionHandler.ts b/packages/uikit-chat-hooks/src/handler/useConnectionHandler.ts index c3af186fb..a6921bb5c 100644 --- a/packages/uikit-chat-hooks/src/handler/useConnectionHandler.ts +++ b/packages/uikit-chat-hooks/src/handler/useConnectionHandler.ts @@ -8,7 +8,7 @@ export const useConnectionHandler = ( handlerId: string, hookHandler: Partial, ) => { - const handlerRef = useRef>(); + const handlerRef = useRef | undefined>(undefined); useLayoutEffect(() => { handlerRef.current = hookHandler; }); diff --git a/packages/uikit-react-native-foundation/src/components/ImageWithPlaceholder/index.tsx b/packages/uikit-react-native-foundation/src/components/ImageWithPlaceholder/index.tsx index db5f2a737..d7f4a6d0e 100644 --- a/packages/uikit-react-native-foundation/src/components/ImageWithPlaceholder/index.tsx +++ b/packages/uikit-react-native-foundation/src/components/ImageWithPlaceholder/index.tsx @@ -15,7 +15,7 @@ const useRetry = (hasError: boolean, retryCount = 5) => { const forceUpdate = useForceUpdate(); const retryCountRef = useRef(1); - const retryTimeoutRef = useRef(); + const retryTimeoutRef = useRef(undefined); useEffect(() => { if (hasError) { diff --git a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx index 1b2158b27..1c6d10838 100644 --- a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx @@ -1,8 +1,14 @@ -import React, { ReactElement } from 'react'; +import React from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; const MAX = 4; +type AvatarChildProps = { + size?: number; + square?: boolean; + containerStyle?: StyleProp; +}; + type Props = React.PropsWithChildren<{ size?: number; containerStyle?: StyleProp; @@ -33,7 +39,8 @@ const AvatarGroup = ({ children, containerStyle, size = 56 }: Props) => { if (index + 1 > MAX) return child; if (!React.isValidElement(child)) return child; - if (childAmount === 1) return React.cloneElement(child as ReactElement, { size, containerStyle }); + if (childAmount === 1) + return React.cloneElement(child as React.ReactElement, { size, containerStyle }); const top = getTopPoint(index, childAmount) * size; const start = getStartPoint(index) * size; @@ -44,7 +51,7 @@ const AvatarGroup = ({ children, containerStyle, size = 56 }: Props) => { return ( - {React.cloneElement(child as ReactElement, { + {React.cloneElement(child as React.ReactElement, { size, square: true, containerStyle: { start: innerStart, top: innerTop }, diff --git a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx index fb059f591..dae392378 100644 --- a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx @@ -1,9 +1,14 @@ -import React, { ReactElement } from 'react'; +import React from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; import Text from '../../components/Text'; import useUIKitTheme from '../../theme/useUIKitTheme'; +type AvatarChildProps = { + size?: number; + containerStyle?: StyleProp; +}; + const DEFAULT_MAX = 3; const DEFAULT_BORDER_WIDTH = 2; const DEFAULT_AVATAR_GAP = -4; @@ -49,7 +54,7 @@ const AvatarStack = ({ const renderAvatars = () => { return childrenArray.slice(0, maxAvatar).map((child, index) => - React.cloneElement(child as ReactElement, { + React.cloneElement(child as React.ReactElement, { size: actualSize, containerStyle: { start: actualGap * index, diff --git a/packages/uikit-react-native-foundation/src/ui/Dialog/index.tsx b/packages/uikit-react-native-foundation/src/ui/Dialog/index.tsx index 3eca8f606..fcb122c4f 100644 --- a/packages/uikit-react-native-foundation/src/ui/Dialog/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Dialog/index.tsx @@ -53,8 +53,8 @@ type Props = React.PropsWithChildren<{ }>; const DISMISS_TIMEOUT = 3000; export const DialogProvider = ({ defaultLabels, children }: Props) => { - const waitDismissTimeout = useRef(); - const waitDismissPromise = useRef<() => void>(); + const waitDismissTimeout = useRef(undefined); + const waitDismissPromise = useRef<(() => void) | undefined>(undefined); const waitDismiss = useCallback((resolver: () => void) => { waitDismissPromise.current = resolver; waitDismissTimeout.current = setTimeout(completeDismiss, DISMISS_TIMEOUT); @@ -68,7 +68,7 @@ export const DialogProvider = ({ defaultLabels, children }: Props) => { const render = useForceUpdate(); const dialogQueue = useRef([]); - const workingDialogJob = useRef(); + const workingDialogJob = useRef(undefined); const visibleState = useRef(false); const isProcessing = () => Boolean(workingDialogJob.current); diff --git a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx index 1aeac53a0..21b58dd3a 100644 --- a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx @@ -526,5 +526,7 @@ const styles = createStyleSheet({ }, }); -// NOTE: Due to Generic inference is not working on forwardRef, we need to cast it as typeof ChannelMessageList and implicit `ref` prop -export default React.forwardRef(ChannelMessageList) as typeof ChannelMessageList; +// NOTE: Due to Generic inference is not working on forwardRef, we need to cast it properly for React 19 compatibility +export default React.forwardRef(ChannelMessageList) as ( + props: ChannelMessageListProps, +) => React.ReactElement | null; diff --git a/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx index 7dcc0c20e..e2a96e3e6 100644 --- a/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx @@ -409,5 +409,7 @@ const styles = createStyleSheet({ }, }); -// NOTE: Due to Generic inference is not working on forwardRef, we need to cast it as typeof ChannelMessageList and implicit `ref` prop -export default React.forwardRef(ChannelThreadMessageList) as typeof ChannelThreadMessageList; +// NOTE: Due to Generic inference is not working on forwardRef, we need to cast it properly for React 19 compatibility +export default React.forwardRef(ChannelThreadMessageList) as ( + props: ChannelThreadMessageListProps, +) => React.ReactElement | null; diff --git a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx index abd218cad..6232f3711 100644 --- a/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx +++ b/packages/uikit-react-native/src/components/FileViewer/FileViewerContent.tsx @@ -89,7 +89,7 @@ const ZoomableImageView = ({ }) => { const { width, height } = useWindowDimensions(); - const imageSize = useRef<{ width: number; height: number }>(); + const imageSize = useRef<{ width: number; height: number } | undefined>(undefined); const [contentSizeProps, setContentSizeProps] = useState<{ contentWidth: number; contentHeight: number; diff --git a/packages/uikit-react-native/src/components/ReactionBottomSheets/ReactionUserListBottomSheet.tsx b/packages/uikit-react-native/src/components/ReactionBottomSheets/ReactionUserListBottomSheet.tsx index 9d393036a..675e9a5ca 100644 --- a/packages/uikit-react-native/src/components/ReactionBottomSheets/ReactionUserListBottomSheet.tsx +++ b/packages/uikit-react-native/src/components/ReactionBottomSheets/ReactionUserListBottomSheet.tsx @@ -34,7 +34,7 @@ const ReactionUserListBottomSheet = ({ const { colors } = useUIKitTheme(); const [tabIndex, setTabIndex] = useState(0); - const scrollRef = useRef(); + const scrollRef = useRef(undefined); const tabIndicatorValue = useRef>([]); const tabIndicatorAnimated = useRef({ x: new Animated.Value(0), width: new Animated.Value(0) }).current; const focusedWithLayoutCalculated = useRef(false); diff --git a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx index ab77c5075..ff129d321 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -37,7 +37,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { const isNewLineExistInChannelRef = useRef(false); const scrolledAwayFromBottomRef = useRef(false); const [isVisibleUnreadMessageFloating, setIsVisibleUnreadMessageFloating] = useState(false); - const viewableMessages = useRef(); + const viewableMessages = useRef(undefined); const hasUserMarkedAsUnreadRef = useRef(false); const [unreadFirstMessage, setUnreadFirstMessage] = useState(undefined); const pendingBottomReachedRef = useRef<{ timeout: number; timestamp: number } | null>(null); @@ -216,7 +216,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { isNewLineExistInChannelRef.current = !!props.isNewLineExistInChannel && !!viewableMessages.current; }, [props.isNewLineExistInChannel, viewableMessages.current]); - const unreadMessagesFloatingPropsRef = useRef(); + const unreadMessagesFloatingPropsRef = useRef(undefined); const updateUnreadMessagesFloatingProps = useFreshCallback(() => { const canAutoMarkAsRead = !scrolledAwayFromBottomRef.current && diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx index 596602938..5b9e6a50f 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx @@ -29,7 +29,7 @@ const createGroupChannelMembersFragment = ( sortComparator, queryCreator = () => channel.createMemberListQuery({ limit: 20 }), }) => { - const refreshSchedule = useRef(); + const refreshSchedule = useRef(undefined); const { STRINGS } = useLocalization(); const { sdk, currentUser } = useSendbirdChat(); const { openMenu } = useActionMenu(); diff --git a/packages/uikit-react-native/src/fragments/createOpenChannelParticipantsFragment.tsx b/packages/uikit-react-native/src/fragments/createOpenChannelParticipantsFragment.tsx index 06777f409..e4092ab75 100644 --- a/packages/uikit-react-native/src/fragments/createOpenChannelParticipantsFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createOpenChannelParticipantsFragment.tsx @@ -29,7 +29,7 @@ const createOpenChannelParticipantsFragment = ( }) => { const handlerId = useUniqHandlerId('OpenChannelParticipantsFragment'); - const refreshSchedule = useRef(); + const refreshSchedule = useRef(undefined); const { STRINGS } = useLocalization(); const { sdk, currentUser } = useSendbirdChat(); const { openMenu } = useActionMenu(); diff --git a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts index 22c98be51..48eaf9562 100644 --- a/packages/uikit-react-native/src/hooks/useMentionTextInput.ts +++ b/packages/uikit-react-native/src/hooks/useMentionTextInput.ts @@ -12,7 +12,7 @@ const useMentionTextInput = (params: { messageToEdit?: SendbirdUserMessage | Sen const { mentionManager, sbOptions } = useSendbirdChat(); const mentionedUsersRef = useRef([]); - const textInputRef = useRef(); + const textInputRef = useRef(undefined); const [text, setText] = useState(''); const [selection, setSelection] = useState({ start: 0, end: 0 }); diff --git a/packages/uikit-react-native/src/hooks/usePushTokenRegistration.ts b/packages/uikit-react-native/src/hooks/usePushTokenRegistration.ts index 84a85a7dd..632b04fc1 100644 --- a/packages/uikit-react-native/src/hooks/usePushTokenRegistration.ts +++ b/packages/uikit-react-native/src/hooks/usePushTokenRegistration.ts @@ -9,7 +9,7 @@ const usePushTokenRegistration = () => { const { sdk } = useSendbirdChat(); const { notificationService } = usePlatformService(); - const refreshListener = useRef<() => void>(); + const refreshListener = useRef<(() => void) | undefined>(undefined); const [registerToken, unregisterToken, getToken] = useIIFE(() => { return [ Platform.select({ diff --git a/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts b/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts index ef715442d..a19200e26 100644 --- a/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts +++ b/packages/uikit-react-native/src/hooks/useVoiceMessageInput.ts @@ -76,7 +76,7 @@ const useVoiceMessageInput = ({ onSend, onClose }: Props): VoiceMessageInputResu duration: 0, }); - const recordingPath = useRef<{ recordFilePath: string; uri: string }>(); + const recordingPath = useRef<{ recordFilePath: string; uri: string } | undefined>(undefined); const getVoiceMessageRecordingPath = () => { if (!recordingPath.current) throw new Error('No recording path'); return recordingPath.current; diff --git a/packages/uikit-utils/src/hooks/index.ts b/packages/uikit-utils/src/hooks/index.ts index 2d8ef878c..4db53ae61 100644 --- a/packages/uikit-utils/src/hooks/index.ts +++ b/packages/uikit-utils/src/hooks/index.ts @@ -79,7 +79,7 @@ export const useFreshCallback = (callback: T): T => { }; export const useDebounceEffect = (action: () => void, delay: number, deps: DependencyList = []) => { - const timeoutRef = useRef(); + const timeoutRef = useRef(undefined); useEffect(() => { timeoutRef.current = setTimeout(async () => { diff --git a/packages/uikit-utils/src/hooks/react-native.ts b/packages/uikit-utils/src/hooks/react-native.ts index 3d8cc0f71..560e19b94 100644 --- a/packages/uikit-utils/src/hooks/react-native.ts +++ b/packages/uikit-utils/src/hooks/react-native.ts @@ -69,7 +69,7 @@ export const useAppState = (type: AppStateEvent, listener: AppStateListener) => * To achieve this, you can use a deferred onClose that can be awaited until the onDismiss is called. * */ export const useDeferredModalState = () => { - const resolveRef = useRef<(value: void) => void>(); + const resolveRef = useRef<((value: void) => void) | undefined>(undefined); const [visible, setVisible] = useState(false); return { diff --git a/yarn.lock b/yarn.lock index ddc05794f..105d81fb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1182,7 +1182,7 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3", "@babel/traverse@^7.13.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4": +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== @@ -1211,6 +1211,19 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.13.0", "@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" + "@babel/types@7.17.0": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" @@ -4325,11 +4338,6 @@ resolved "https://registry.yarnpkg.com/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#ee1bd8c9f7a01b3445786aad0ef23aba5f511a44" integrity sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA== -"@types/prop-types@*": - version "15.7.15" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" - integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== - "@types/qs@^6.9.5": version "6.14.0" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" @@ -4349,13 +4357,12 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.0.0", "@types/react@^18": - version "18.3.26" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.26.tgz#4c5970878d30db3d2a0bca1e4eb5f258e391bbeb" - integrity sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA== +"@types/react@*", "@types/react@>=16.0.0", "@types/react@^19.1.1": + version "19.2.7" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f" + integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg== dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" + csstype "^3.2.2" "@types/semver@^7.5.0": version "7.7.1" @@ -6739,10 +6746,10 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -csstype@^3.0.2: - version "3.2.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.1.tgz#d19b5c2b584e8f57361e7129e160a75aba418e40" - integrity sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g== +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== cyclist@^1.0.1: version "1.0.2" @@ -14491,7 +14498,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14603,7 +14619,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14617,6 +14633,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -15757,7 +15780,7 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15775,6 +15798,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 5de670a4938d86bfb6c9e8d1d5b7774b458f4a3a Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Mon, 22 Dec 2025 11:50:19 +0900 Subject: [PATCH 2/4] chore: update useRef types for React 19 compatibility --- .../channel/useChannelMessagesReducer.test.ts | 2 +- .../src/mocks/createMockChannel.ts | 17 ++++++++++++++--- .../src/mocks/createMockMessage.ts | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts b/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts index bb2b2038b..6c58ba1f1 100644 --- a/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts +++ b/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts @@ -285,7 +285,7 @@ describe('useChannelMessagesReducer', () => { const updatedMessages = [ createMockMessage({ ...sentMessages[0], - reactions: [{ key: 'string', userIds: [], updatedAt: Date.now(), isEmpty: false, applyEvent: jest.fn() }], + reactions: [{ key: 'string', userIds: [], updatedAt: Date.now(), isEmpty: false, applyEvent: jest.fn(), sampledUserIds: [], sampledUserInfoList: [], count: 0, hasCurrentUserReacted: false }], }), ]; diff --git a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts index f874c144a..34cb96fd5 100644 --- a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts +++ b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts @@ -122,6 +122,19 @@ class MockChannel implements GetMockProps { const query = createMockQuery({ type: 'message', dataLength: 300, diff --git a/packages/uikit-testing-tools/src/mocks/createMockMessage.ts b/packages/uikit-testing-tools/src/mocks/createMockMessage.ts index 113273409..30e1c2534 100644 --- a/packages/uikit-testing-tools/src/mocks/createMockMessage.ts +++ b/packages/uikit-testing-tools/src/mocks/createMockMessage.ts @@ -73,6 +73,17 @@ class MockMessage implements GetMockProps { myFeedback: Feedback | null = null; myFeedbackStatus: FeedbackStatus = 'NO_FEEDBACK'; suggestedReplies: string[] | null = null; + messageForm = null; + message = ''; + poll = null; + + applyPoll(): boolean { + return false; + } + + submitMessageForm(): Promise { + return Promise.resolve(); + } isFileMessage(): this is SendbirdFileMessage { return this.messageType === MessageType.FILE && !Object.prototype.hasOwnProperty.call(this, 'fileInfoList'); From 0c6abd23bb01989cb60bd2548f2c7b95c609a5cc Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Mon, 22 Dec 2025 11:54:31 +0900 Subject: [PATCH 3/4] chore: update useRef types for React 19 compatibility --- .../channel/useChannelMessagesReducer.test.ts | 14 +++++- .../src/ui/Avatar/AvatarGroup.tsx | 3 +- .../src/mocks/createMockChannel.ts | 50 ++++++++++--------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts b/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts index 6c58ba1f1..cf4bf883a 100644 --- a/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts +++ b/packages/uikit-chat-hooks/src/__tests__/channel/useChannelMessagesReducer.test.ts @@ -285,7 +285,19 @@ describe('useChannelMessagesReducer', () => { const updatedMessages = [ createMockMessage({ ...sentMessages[0], - reactions: [{ key: 'string', userIds: [], updatedAt: Date.now(), isEmpty: false, applyEvent: jest.fn(), sampledUserIds: [], sampledUserInfoList: [], count: 0, hasCurrentUserReacted: false }], + reactions: [ + { + key: 'string', + userIds: [], + updatedAt: Date.now(), + isEmpty: false, + applyEvent: jest.fn(), + sampledUserIds: [], + sampledUserInfoList: [], + count: 0, + hasCurrentUserReacted: false, + }, + ], }), ]; diff --git a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx index 1c6d10838..759e1d526 100644 --- a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarGroup.tsx @@ -39,8 +39,9 @@ const AvatarGroup = ({ children, containerStyle, size = 56 }: Props) => { if (index + 1 > MAX) return child; if (!React.isValidElement(child)) return child; - if (childAmount === 1) + if (childAmount === 1) { return React.cloneElement(child as React.ReactElement, { size, containerStyle }); + } const top = getTopPoint(index, childAmount) * size; const start = getStartPoint(index) * size; diff --git a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts index 34cb96fd5..b6190c242 100644 --- a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts +++ b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts @@ -240,30 +240,32 @@ class MockChannel implements GetMockProps { - const query = createMockQuery({ - type: 'message', - dataLength: 300, - limit: params?.limit, - sdk: this.params.sdk, - }); - return { - reverse: false, - channelType: ChannelType.BASE, - channelUrl: 'channel_url_' + tc.getHash(), - customTypesFilter: [], - includeMetaArray: false, - includeParentMessageInfo: false, - includeReactions: false, - includeThreadInfo: false, - messageTypeFilter: MessageTypeFilter.ALL, - replyType: ReplyType.NONE, - senderUserIdsFilter: [], - showSubchannelMessagesOnly: false, - load: query.next, - ...query, - }; - }); + createPreviousMessageListQuery = jest.fn( + (params?: PreviousMessageListQueryParams | undefined): PreviousMessageListQuery => { + const query = createMockQuery({ + type: 'message', + dataLength: 300, + limit: params?.limit, + sdk: this.params.sdk, + }); + return { + reverse: false, + channelType: ChannelType.BASE, + channelUrl: 'channel_url_' + tc.getHash(), + customTypesFilter: [], + includeMetaArray: false, + includeParentMessageInfo: false, + includeReactions: false, + includeThreadInfo: false, + messageTypeFilter: MessageTypeFilter.ALL, + replyType: ReplyType.NONE, + senderUserIdsFilter: [], + showSubchannelMessagesOnly: false, + load: query.next, + ...query, + }; + }, + ); createMessageCollection = jest.fn((params?: MessageCollectionParams | undefined): SendbirdMessageCollection => { return createMockMessageCollection({ ...params, From f8860dfde15358fb1983fbf676b87bb36eecf92f Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Mon, 22 Dec 2025 14:59:36 +0900 Subject: [PATCH 4/4] chore: update useRef types for React 19 compatibility --- .../common/useMessageOutgoingStatus.test.ts | 25 +++------ .../src/mocks/createMockChannel.ts | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/packages/uikit-chat-hooks/src/__tests__/common/useMessageOutgoingStatus.test.ts b/packages/uikit-chat-hooks/src/__tests__/common/useMessageOutgoingStatus.test.ts index addeb5721..5e10be510 100644 --- a/packages/uikit-chat-hooks/src/__tests__/common/useMessageOutgoingStatus.test.ts +++ b/packages/uikit-chat-hooks/src/__tests__/common/useMessageOutgoingStatus.test.ts @@ -1,4 +1,4 @@ -import { act, renderHook, waitFor } from '@testing-library/react-native'; +import { renderHook, waitFor } from '@testing-library/react-native'; import { ChannelType } from '@sendbird/chat'; import { SendingStatus } from '@sendbird/chat/message'; @@ -174,26 +174,17 @@ describe('useMessageOutgoingStatus', () => { renderHook(() => useMessageOutgoingStatus(sdk, channel, message)); - act(() => { - sdk.__emit('channel', 'group_onUndeliveredMemberStatusUpdated', channel); - }); - + sdk.__emit('channel', 'group_onUndeliveredMemberStatusUpdated', channel); await waitFor(() => { expect(forceUpdate).toHaveBeenCalledTimes(1); }); - act(() => { - sdk.__emit('channel', 'group_onUserMarkedRead', channel); - }); - + sdk.__emit('channel', 'group_onUserMarkedRead', channel); await waitFor(() => { expect(forceUpdate).toHaveBeenCalledTimes(2); }); - act(() => { - sdk.__emit('channel', 'group_onUserMarkedUnread', channel); - }); - + sdk.__emit('channel', 'group_onUserMarkedUnread', channel); await waitFor(() => { expect(forceUpdate).toHaveBeenCalledTimes(3); }); @@ -226,11 +217,9 @@ describe('useMessageOutgoingStatus', () => { const { unmount } = renderHook(() => useMessageOutgoingStatus(sdk, channel, message)); unmount(); - act(() => { - sdk.__emit('channel', 'group_onUndeliveredMemberStatusUpdated', channel); - sdk.__emit('channel', 'group_onUserMarkedRead', channel); - sdk.__emit('channel', 'group_onUserMarkedUnread', channel); - }); + sdk.__emit('channel', 'group_onUndeliveredMemberStatusUpdated', channel); + sdk.__emit('channel', 'group_onUserMarkedRead', channel); + sdk.__emit('channel', 'group_onUserMarkedUnread', channel); await waitFor(() => { expect(forceUpdate).not.toHaveBeenCalled(); diff --git a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts index b6190c242..030c9eef0 100644 --- a/packages/uikit-testing-tools/src/mocks/createMockChannel.ts +++ b/packages/uikit-testing-tools/src/mocks/createMockChannel.ts @@ -27,6 +27,7 @@ import { } from '@sendbird/chat/groupChannel'; import { BaseListQueryParams, + Conversation, DeliveryStatus, MultipleFilesMessageCreateParams, MultipleFilesMessageRequestHandler, @@ -135,6 +136,58 @@ class MockChannel implements GetMockProps { + return Promise.resolve(); + } + + getMessageDeletionTimestamp(): Promise { + return Promise.resolve(this.messageDeletionTimestamp); + } + + submitCSAT(): Promise { + return Promise.resolve(this.conversation as Conversation); + } + + markConversationAsHandoff(): Promise { + return Promise.resolve(this.conversation as Conversation); + } + + closeConversation(): Promise { + return Promise.resolve(this.conversation as Conversation); + } + + startConversation(): Promise { + return Promise.resolve(this.conversation as Conversation); + } + + resumeConversation(): Promise { + return Promise.resolve(this.conversation as Conversation); + } + + endConversation(): Promise { + return Promise.resolve(this.conversation as Conversation); + } + + getContextObject(_aiAgentId: string): Promise { + return Promise.resolve({} as T); + } + + updateContext(_aiAgentId: string, _context: Record): Promise { + return Promise.resolve({} as T); + } + + patchContext(_aiAgentId: string, _context: Record): Promise { + return Promise.resolve({} as T); + } serialize(): object { throw new Error('Method not implemented.');