From 9223b438f78d8b63da778c3c74329bdb383ba997 Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 14 Nov 2023 17:27:33 +0900 Subject: [PATCH 1/3] feat: add typing indicator bubble ui and logic --- .../src/index.ts | 8 +- .../src/ui/Avatar/AvatarStack.tsx | 95 ++++++++++++++++ .../src/ui/Avatar/index.tsx | 2 + .../src/ui/MessageTypingBubble/index.tsx | 106 ++++++++++++++++++ packages/uikit-react-native/package.json | 2 +- .../components/ChannelMessageList/index.tsx | 2 + .../GroupChannelMessageRenderer/index.tsx | 22 +++- .../component/GroupChannelHeader.tsx | 16 ++- packages/uikit-utils/src/types.ts | 4 +- sample/src/App.tsx | 1 + yarn.lock | 8 +- 11 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx create mode 100644 packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx diff --git a/packages/uikit-react-native-foundation/src/index.ts b/packages/uikit-react-native-foundation/src/index.ts index 47d860989..8e8187999 100644 --- a/packages/uikit-react-native-foundation/src/index.ts +++ b/packages/uikit-react-native-foundation/src/index.ts @@ -26,14 +26,14 @@ export { default as BottomSheet } from './ui/BottomSheet'; export { default as Button } from './ui/Button'; export { default as ChannelFrozenBanner } from './ui/ChannelFrozenBanner'; export { DialogProvider, useActionMenu, useAlert, usePrompt, useBottomSheet } from './ui/Dialog'; +export { default as GroupChannelMessage } from './ui/GroupChannelMessage'; +export type { GroupChannelMessageProps } from './ui/GroupChannelMessage'; +export { default as GroupChannelPreview } from './ui/GroupChannelPreview'; export { default as Header } from './ui/Header'; export { default as LoadingSpinner } from './ui/LoadingSpinner'; export { default as MenuBar } from './ui/MenuBar'; export type { MenuBarProps } from './ui/MenuBar'; -export { default as GroupChannelMessage } from './ui/GroupChannelMessage'; -export type { GroupChannelMessageProps } from './ui/GroupChannelMessage'; -export { default as GroupChannelPreview } from './ui/GroupChannelPreview'; - +export { default as MessageTypingBubble } from './ui/MessageTypingBubble'; export { default as OpenChannelMessage } from './ui/OpenChannelMessage'; export type { OpenChannelMessageProps } from './ui/OpenChannelMessage'; export { default as OpenChannelPreview } from './ui/OpenChannelPreview'; diff --git a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx new file mode 100644 index 000000000..cff094a97 --- /dev/null +++ b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx @@ -0,0 +1,95 @@ +import React, { ReactElement } from 'react'; +import { StyleProp, View, ViewStyle } from 'react-native'; + +import Text from '../../components/Text'; +import useUIKitTheme from '../../theme/useUIKitTheme'; + +const DEFAULT_MAX = 3; +const DEFAULT_BORDER_WIDTH = 2; +const DEFAULT_AVATAR_GAP = -4; +const DEFAULT_AVATAR_SIZE = 26; +const DEFAULT_REMAINS_MAX = 99; + +type Props = React.PropsWithChildren<{ + size?: number; + containerStyle?: StyleProp; + maxAvatar?: number; + avatarGap?: number; + styles?: { + borderWidth?: number; + borderColor?: string; + }; +}>; + +const AvatarStack = ({ + children, + containerStyle, + styles, + maxAvatar = DEFAULT_MAX, + size = DEFAULT_AVATAR_SIZE, + avatarGap = DEFAULT_AVATAR_GAP, +}: Props) => { + const { colors, palette } = useUIKitTheme(); + const defaultStyles = { borderWidth: DEFAULT_BORDER_WIDTH, borderColor: colors.background }; + const avatarStyles = { ...defaultStyles, ...styles }; + + const childrenArray = React.Children.toArray(children).filter((it) => React.isValidElement(it)); + const remains = childrenArray.length - maxAvatar; + const shouldRenderRemains = remains > 0; + + const actualGap = avatarGap - avatarStyles.borderWidth; + + const renderAvatars = () => { + return childrenArray.slice(0, maxAvatar).map((child, index) => + React.cloneElement(child as ReactElement, { + size, + containerStyle: { + left: actualGap * index, + borderWidth: avatarStyles.borderWidth, + borderColor: avatarStyles.borderColor, + }, + }), + ); + }; + + const renderRemainsCount = () => { + if (!shouldRenderRemains) return null; + return ( + + + {`+${Math.min(remains, DEFAULT_REMAINS_MAX)}`} + + + ); + }; + + const calculateWidth = () => { + const widthEach = size + actualGap; + const avatarCountOffset = shouldRenderRemains ? 1 : 0; + const avatarCount = shouldRenderRemains ? maxAvatar : childrenArray.length; + const count = avatarCount + avatarCountOffset; + return widthEach * count - actualGap; + }; + + return ( + + {renderAvatars()} + {renderRemainsCount()} + + ); +}; + +export default AvatarStack; diff --git a/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx b/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx index e819ed179..f17365cc0 100644 --- a/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Avatar/index.tsx @@ -9,6 +9,7 @@ import createStyleSheet from '../../styles/createStyleSheet'; import useUIKitTheme from '../../theme/useUIKitTheme'; import AvatarGroup from './AvatarGroup'; import AvatarIcon from './AvatarIcon'; +import AvatarStack from './AvatarStack'; type Props = { uri?: string; @@ -68,4 +69,5 @@ const styles = createStyleSheet({ export default Object.assign(Avatar, { Group: AvatarGroup, Icon: AvatarIcon, + Stack: AvatarStack, }); diff --git a/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx b/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx new file mode 100644 index 000000000..6f80702c2 --- /dev/null +++ b/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, Easing, StyleProp, ViewStyle } from 'react-native'; + +import type { SendbirdUser } from '@sendbird/uikit-utils'; + +import Box from '../../components/Box'; +import createStyleSheet from '../../styles/createStyleSheet'; +import useUIKitTheme from '../../theme/useUIKitTheme'; +import Avatar from '../Avatar'; + +type Props = { + typingUsers: SendbirdUser[]; + containerStyle?: StyleProp; + styles?: {}; + + maxAvatar?: number; +}; + +const MessageTypingBubble = ({ typingUsers, containerStyle, maxAvatar }: Props) => { + if (typingUsers.length === 0) return null; + + return ( + + + {typingUsers.map((user, index) => ( + + ))} + + + + ); +}; + +const TypingDots = () => { + const { select, palette, colors } = useUIKitTheme(); + const animation = useRef(new Animated.Value(0)).current; + const dots = matrix.map(([timeline, scale, opacity]) => [ + animation.interpolate({ inputRange: timeline, outputRange: scale, extrapolate: 'clamp' }), + animation.interpolate({ inputRange: timeline, outputRange: opacity, extrapolate: 'clamp' }), + ]); + + useEffect(() => { + const animated = Animated.loop( + Animated.timing(animation, { toValue: 1.4, duration: 1400, easing: Easing.linear, useNativeDriver: true }), + ); + animated.start(); + return () => animated.reset(); + }, []); + + return ( + + {dots.map(([scale, opacity], index) => { + return ( + + ); + })} + + ); +}; + +const matrix = [ + [ + [0.4, 0.7, 1.0], + [1.0, 1.2, 1.0], + [0.12, 0.38, 0.12], + ], + [ + [0.6, 0.9, 1.2], + [1.0, 1.2, 1.0], + [0.12, 0.38, 0.12], + ], + [ + [0.8, 1.1, 1.4], + [1.0, 1.2, 1.0], + [0.12, 0.38, 0.12], + ], +]; + +const styles = createStyleSheet({ + dot: { + width: 8, + height: 8, + borderRadius: 4, + }, +}); + +export default MessageTypingBubble; diff --git a/packages/uikit-react-native/package.json b/packages/uikit-react-native/package.json index 015103138..613ed2c0f 100644 --- a/packages/uikit-react-native/package.json +++ b/packages/uikit-react-native/package.json @@ -61,7 +61,7 @@ "dependencies": { "@sendbird/uikit-chat-hooks": "3.2.0", "@sendbird/uikit-react-native-foundation": "3.2.0", - "@sendbird/uikit-tools": "^0.0.1-alpha.38", + "@sendbird/uikit-tools": "0.0.1-alpha.42", "@sendbird/uikit-utils": "3.2.0" }, "devDependencies": { diff --git a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx index d18e39088..98315300a 100644 --- a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx @@ -76,6 +76,7 @@ export type ChannelMessageListProps['currentUserId']; enableMessageGrouping: ChannelMessageListProps['enableMessageGrouping']; bottomSheetItem?: BottomSheetItem; + isFirstItem: boolean; }) => React.ReactElement | null; renderNewMessagesButton: null | CommonComponent<{ visible: boolean; @@ -150,6 +151,7 @@ const ChannelMessageList = { + const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); const playerUnsubscribes = useRef<(() => void)[]>([]); const { palette } = useUIKitTheme(); const { sbOptions, currentUser, mentionManager } = useSendbirdChat(); @@ -284,10 +293,19 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' } }); + const renderTypingBubble = () => { + if (!isFirstItem) return null; + if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null; + if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has('bubble')) return null; + + return ; + }; + return ( {renderMessage()} + {renderTypingBubble()} ); }; diff --git a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx index 046af5be2..241820855 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx @@ -4,7 +4,7 @@ import { View } from 'react-native'; import { Header, Icon, createStyleSheet, useHeaderStyle } from '@sendbird/uikit-react-native-foundation'; import ChannelCover from '../../../components/ChannelCover'; -import { useLocalization } from '../../../hooks/useContext'; +import { useLocalization, useSendbirdChat } from '../../../hooks/useContext'; import { GroupChannelContexts } from '../module/moduleContext'; import type { GroupChannelProps } from '../types'; @@ -13,11 +13,21 @@ const GroupChannelHeader = ({ onPressHeaderLeft, onPressHeaderRight, }: GroupChannelProps['Header']) => { + const { sbOptions } = useSendbirdChat(); const { headerTitle, channel } = useContext(GroupChannelContexts.Fragment); const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); const { STRINGS } = useLocalization(); const { HeaderComponent } = useHeaderStyle(); - const subtitle = STRINGS.LABELS.TYPING_INDICATOR_TYPINGS(typingUsers); + + const renderSubtitle = () => { + const subtitle = STRINGS.LABELS.TYPING_INDICATOR_TYPINGS(typingUsers); + + if (!subtitle) return null; + if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null; + if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has('text')) return null; + + return {subtitle}; + }; const isHidden = shouldHideRight(); @@ -29,7 +39,7 @@ const GroupChannelHeader = ({ {headerTitle} - {Boolean(subtitle) && subtitle && {subtitle}} + {renderSubtitle()} } diff --git a/packages/uikit-utils/src/types.ts b/packages/uikit-utils/src/types.ts index 906b4b0a8..f5cb06213 100644 --- a/packages/uikit-utils/src/types.ts +++ b/packages/uikit-utils/src/types.ts @@ -60,7 +60,9 @@ export type UnionToIntersection = (U extends unknown ? (k: U) => void : never export type OmittedValues = Omit[keyof Omit]; export type PartialDeep = T extends object - ? T extends Function + ? T extends Set + ? T + : T extends Function ? T : { [P in keyof T]?: PartialDeep; diff --git a/sample/src/App.tsx b/sample/src/App.tsx index e617fe5a1..1a66dde99 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -60,6 +60,7 @@ const App = () => { }, groupChannel: { enableMention: true, + typingIndicatorTypes: new Set(['bubble']), }, groupChannelList: { enableTypingIndicator: true, diff --git a/yarn.lock b/yarn.lock index ce9f4cca9..3682bd4d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3382,10 +3382,10 @@ resolved "https://registry.yarnpkg.com/@sendbird/react-native-scrollview-enhancer/-/react-native-scrollview-enhancer-0.2.1.tgz#25de4af78293978a4c0ef6fddee25d822a364c46" integrity sha512-LN+Tm+ZUkE2MBVreg/JI8SVr8SOKRteZN0YFpGzRtbKkP45+pKyPN4JQPf73eFx7qO8zDL+TUVyzz/1MOnIK7g== -"@sendbird/uikit-tools@^0.0.1-alpha.38": - version "0.0.1-alpha.39" - resolved "https://registry.yarnpkg.com/@sendbird/uikit-tools/-/uikit-tools-0.0.1-alpha.39.tgz#1982e2e9d752779d0229fee39349fbac25836f92" - integrity sha512-hHHeYXEF8my7EfX/jkLfC7iWo1G7GKITL6ZMA3rw2AIHbCTuoF+qzB9r5sE1KZYkXDL4spsZDP7FXv2fqGRyiw== +"@sendbird/uikit-tools@0.0.1-alpha.42": + version "0.0.1-alpha.42" + resolved "https://registry.yarnpkg.com/@sendbird/uikit-tools/-/uikit-tools-0.0.1-alpha.42.tgz#dc4bdedd8a08c8681524cd5d27e68f813a40a52a" + integrity sha512-4XGuw5qbFbPSnpM6SdXoX9UKHwFD0cXKHS7BSfdnmJ7oF+6slpj5TthigWL8/+rhlYb/RLbwE7gqCOllnr+l2w== "@sideway/address@^4.1.3": version "4.1.4" From 51298fc071d044ddf66ed9aed6345b494866560d Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 14 Nov 2023 18:27:21 +0900 Subject: [PATCH 2/3] chore: apply details --- .../src/ui/Avatar/AvatarStack.tsx | 30 ++++++++++++------- .../src/ui/MessageTypingBubble/index.tsx | 30 ++++++++++++++----- .../GroupChannelMessageRenderer/index.tsx | 26 +++++++++------- packages/uikit-react-native/src/constants.ts | 5 ++++ .../fragments/createGroupChannelFragment.tsx | 14 +++++++-- packages/uikit-react-native/src/index.ts | 2 +- sample/src/App.tsx | 4 +-- 7 files changed, 75 insertions(+), 36 deletions(-) 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 cff094a97..42e12ab10 100644 --- a/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Avatar/AvatarStack.tsx @@ -18,6 +18,8 @@ type Props = React.PropsWithChildren<{ styles?: { borderWidth?: number; borderColor?: string; + remainsTextColor?: string; + remainsBackgroundColor?: string; }; }>; @@ -29,20 +31,26 @@ const AvatarStack = ({ size = DEFAULT_AVATAR_SIZE, avatarGap = DEFAULT_AVATAR_GAP, }: Props) => { - const { colors, palette } = useUIKitTheme(); - const defaultStyles = { borderWidth: DEFAULT_BORDER_WIDTH, borderColor: colors.background }; + const { colors, palette, select } = useUIKitTheme(); + const defaultStyles = { + borderWidth: DEFAULT_BORDER_WIDTH, + borderColor: colors.background, + remainsTextColor: colors.onBackground02, + remainsBackgroundColor: select({ light: palette.background100, dark: palette.background600 }), + }; const avatarStyles = { ...defaultStyles, ...styles }; const childrenArray = React.Children.toArray(children).filter((it) => React.isValidElement(it)); const remains = childrenArray.length - maxAvatar; const shouldRenderRemains = remains > 0; + const actualSize = size + avatarStyles.borderWidth * 2; const actualGap = avatarGap - avatarStyles.borderWidth; const renderAvatars = () => { return childrenArray.slice(0, maxAvatar).map((child, index) => React.cloneElement(child as ReactElement, { - size, + size: actualSize, containerStyle: { left: actualGap * index, borderWidth: avatarStyles.borderWidth, @@ -60,16 +68,16 @@ const AvatarStack = ({ avatarStyles, { left: actualGap * maxAvatar, - width: size, - height: size, - borderRadius: size / 2, + width: actualSize, + height: actualSize, + borderRadius: actualSize / 2, alignItems: 'center', justifyContent: 'center', - backgroundColor: palette.background100, + backgroundColor: avatarStyles.remainsBackgroundColor, }, ]} > - + {`+${Math.min(remains, DEFAULT_REMAINS_MAX)}`} @@ -77,15 +85,15 @@ const AvatarStack = ({ }; const calculateWidth = () => { - const widthEach = size + actualGap; + const widthEach = actualSize + actualGap; const avatarCountOffset = shouldRenderRemains ? 1 : 0; const avatarCount = shouldRenderRemains ? maxAvatar : childrenArray.length; const count = avatarCount + avatarCountOffset; - return widthEach * count - actualGap; + return widthEach * count + avatarStyles.borderWidth; }; return ( - + {renderAvatars()} {renderRemainsCount()} diff --git a/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx b/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx index 6f80702c2..5cb13886f 100644 --- a/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx @@ -11,28 +11,42 @@ import Avatar from '../Avatar'; type Props = { typingUsers: SendbirdUser[]; containerStyle?: StyleProp; - styles?: {}; - maxAvatar?: number; }; const MessageTypingBubble = ({ typingUsers, containerStyle, maxAvatar }: Props) => { + const { select, palette, colors } = useUIKitTheme(); + if (typingUsers.length === 0) return null; return ( - + {typingUsers.map((user, index) => ( ))} - + ); }; -const TypingDots = () => { - const { select, palette, colors } = useUIKitTheme(); +type TypingDotsProps = { + dotColor: string; + backgroundColor: string; +}; +const TypingDots = ({ dotColor, backgroundColor }: TypingDotsProps) => { const animation = useRef(new Animated.Value(0)).current; const dots = matrix.map(([timeline, scale, opacity]) => [ animation.interpolate({ inputRange: timeline, outputRange: scale, extrapolate: 'clamp' }), @@ -55,7 +69,7 @@ const TypingDots = () => { borderRadius={16} paddingHorizontal={12} height={34} - backgroundColor={select({ light: palette.background100, dark: palette.background600 })} + backgroundColor={dotColor} > {dots.map(([scale, opacity], index) => { return ( @@ -67,7 +81,7 @@ const TypingDots = () => { marginRight: index === dots.length - 1 ? 0 : 6, opacity: opacity, transform: [{ scale: scale }], - backgroundColor: colors.onBackground02, + backgroundColor: backgroundColor, }, ]} /> diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx index 2b195c88a..51e28e2c5 100644 --- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx +++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx @@ -44,9 +44,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' focused, prevMessage, nextMessage, - isFirstItem, }) => { - const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); const playerUnsubscribes = useRef<(() => void)[]>([]); const { palette } = useUIKitTheme(); const { sbOptions, currentUser, mentionManager } = useSendbirdChat(); @@ -293,19 +291,25 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' } }); - const renderTypingBubble = () => { - if (!isFirstItem) return null; - if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null; - if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has('bubble')) return null; - - return ; - }; - return ( {renderMessage()} - {renderTypingBubble()} + + ); +}; + +export const GroupChannelMessageTypingBubble = () => { + const { sbOptions } = useSendbirdChat(); + const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); + + if (typingUsers.length === 0) return null; + if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null; + if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has('bubble')) return null; + + return ( + + ); }; diff --git a/packages/uikit-react-native/src/constants.ts b/packages/uikit-react-native/src/constants.ts index fb26b3cf8..aa12a84a8 100644 --- a/packages/uikit-react-native/src/constants.ts +++ b/packages/uikit-react-native/src/constants.ts @@ -5,3 +5,8 @@ export const MESSAGE_FOCUS_ANIMATION_DELAY = 250; export const UNKNOWN_USER_ID = '##__USER_ID_IS_NOT_PROVIDED__##'; export const VOICE_MESSAGE_META_ARRAY_DURATION_KEY = 'KEY_VOICE_MESSAGE_DURATION'; export const VOICE_MESSAGE_META_ARRAY_MESSAGE_TYPE_KEY = 'KEY_INTERNAL_MESSAGE_TYPE'; + +export enum TypingIndicatorType { + Text = 'text', + Bubble = 'bubble', +} diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx index e618b776d..d9af2c2c2 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ReplyType } from '@sendbird/chat/message'; import { useGroupChannelMessages } from '@sendbird/uikit-chat-hooks'; +import { Box } from '@sendbird/uikit-react-native-foundation'; import { NOOP, PASS, @@ -14,7 +15,9 @@ import { useRefTracker, } from '@sendbird/uikit-utils'; -import GroupChannelMessageRenderer from '../components/GroupChannelMessageRenderer'; +import GroupChannelMessageRenderer, { + GroupChannelMessageTypingBubble, +} from '../components/GroupChannelMessageRenderer'; import NewMessagesButton from '../components/NewMessagesButton'; import ScrollToBottomButton from '../components/ScrollToBottomButton'; import StatusComposition from '../components/StatusComposition'; @@ -123,8 +126,13 @@ const createGroupChannelFragment = (initModule?: Partial): G }, []); const renderItem: GroupChannelProps['MessageList']['renderMessage'] = useFreshCallback((props) => { - if (renderMessage) return renderMessage(props); - return ; + const content = renderMessage ? renderMessage(props) : ; + return ( + + {content} + {props.isFirstItem && !hasNext() && } + + ); }); const memoizedFlatListProps = useMemo( diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts index 57e71672b..1bb249df3 100644 --- a/packages/uikit-react-native/src/index.ts +++ b/packages/uikit-react-native/src/index.ts @@ -134,7 +134,7 @@ export { default as SendbirdUIKitContainer, SendbirdUIKit } from './containers/S export type { SendbirdUIKitContainerProps } from './containers/SendbirdUIKitContainer'; export { default as SBUError } from './libs/SBUError'; export { default as SBUUtils } from './libs/SBUUtils'; - +export { TypingIndicatorType } from './constants'; export * from './types'; Logger.setLogLevel(__DEV__ ? 'warn' : 'none'); diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 1a66dde99..fbc348b71 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -4,7 +4,7 @@ import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/ import React, { useEffect } from 'react'; import { AppState } from 'react-native'; -import { SendbirdUIKitContainer, useSendbirdChat } from '@sendbird/uikit-react-native'; +import { SendbirdUIKitContainer, TypingIndicatorType, useSendbirdChat } from '@sendbird/uikit-react-native'; import { DarkUIKitTheme, LightUIKitTheme } from '@sendbird/uikit-react-native-foundation'; // import LogView from './components/LogView'; @@ -60,7 +60,7 @@ const App = () => { }, groupChannel: { enableMention: true, - typingIndicatorTypes: new Set(['bubble']), + typingIndicatorTypes: new Set([TypingIndicatorType.Text, TypingIndicatorType.Bubble]), }, groupChannelList: { enableTypingIndicator: true, From ad085de8ca0f5224f4f5a30b9f6edc342d1039d7 Mon Sep 17 00:00:00 2001 From: bang9 Date: Tue, 21 Nov 2023 17:11:55 +0900 Subject: [PATCH 3/3] chore: update naming --- packages/uikit-react-native-foundation/src/index.ts | 2 +- .../index.tsx | 4 ++-- .../src/components/GroupChannelMessageRenderer/index.tsx | 9 +++++---- packages/uikit-react-native/src/constants.ts | 5 ----- .../domain/groupChannel/component/GroupChannelHeader.tsx | 3 ++- .../src/fragments/createGroupChannelFragment.tsx | 4 ++-- packages/uikit-react-native/src/index.ts | 1 - packages/uikit-react-native/src/types.ts | 5 +++++ 8 files changed, 17 insertions(+), 16 deletions(-) rename packages/uikit-react-native-foundation/src/ui/{MessageTypingBubble => TypingIndicatorBubble}/index.tsx (96%) diff --git a/packages/uikit-react-native-foundation/src/index.ts b/packages/uikit-react-native-foundation/src/index.ts index 8e8187999..ece231992 100644 --- a/packages/uikit-react-native-foundation/src/index.ts +++ b/packages/uikit-react-native-foundation/src/index.ts @@ -33,7 +33,6 @@ export { default as Header } from './ui/Header'; export { default as LoadingSpinner } from './ui/LoadingSpinner'; export { default as MenuBar } from './ui/MenuBar'; export type { MenuBarProps } from './ui/MenuBar'; -export { default as MessageTypingBubble } from './ui/MessageTypingBubble'; export { default as OpenChannelMessage } from './ui/OpenChannelMessage'; export type { OpenChannelMessageProps } from './ui/OpenChannelMessage'; export { default as OpenChannelPreview } from './ui/OpenChannelPreview'; @@ -42,6 +41,7 @@ export { default as Placeholder } from './ui/Placeholder'; export { default as ProfileCard } from './ui/ProfileCard'; export { default as Prompt } from './ui/Prompt'; export { default as Toast, useToast, ToastProvider } from './ui/Toast'; +export { default as TypingIndicatorBubble } from './ui/TypingIndicatorBubble'; /** Styles **/ export { default as createSelectByColorScheme } from './styles/createSelectByColorScheme'; diff --git a/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx b/packages/uikit-react-native-foundation/src/ui/TypingIndicatorBubble/index.tsx similarity index 96% rename from packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx rename to packages/uikit-react-native-foundation/src/ui/TypingIndicatorBubble/index.tsx index 5cb13886f..f27e3c9a8 100644 --- a/packages/uikit-react-native-foundation/src/ui/MessageTypingBubble/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/TypingIndicatorBubble/index.tsx @@ -14,7 +14,7 @@ type Props = { maxAvatar?: number; }; -const MessageTypingBubble = ({ typingUsers, containerStyle, maxAvatar }: Props) => { +const TypingIndicatorBubble = ({ typingUsers, containerStyle, maxAvatar }: Props) => { const { select, palette, colors } = useUIKitTheme(); if (typingUsers.length === 0) return null; @@ -117,4 +117,4 @@ const styles = createStyleSheet({ }, }); -export default MessageTypingBubble; +export default TypingIndicatorBubble; diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx index 51e28e2c5..770e64ca2 100644 --- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx +++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx @@ -4,8 +4,8 @@ import type { GroupChannelMessageProps, RegexTextPattern } from '@sendbird/uikit import { Box, GroupChannelMessage, - MessageTypingBubble, Text, + TypingIndicatorBubble, useUIKitTheme, } from '@sendbird/uikit-react-native-foundation'; import { @@ -27,6 +27,7 @@ import { GroupChannelContexts } from '../../domain/groupChannel/module/moduleCon import type { GroupChannelProps } from '../../domain/groupChannel/types'; import { useLocalization, usePlatformService, useSendbirdChat } from '../../hooks/useContext'; import SBUUtils from '../../libs/SBUUtils'; +import { TypingIndicatorType } from '../../types'; import { ReactionAddons } from '../ReactionAddons'; import GroupChannelMessageDateSeparator from './GroupChannelMessageDateSeparator'; import GroupChannelMessageFocusAnimation from './GroupChannelMessageFocusAnimation'; @@ -299,17 +300,17 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' ); }; -export const GroupChannelMessageTypingBubble = () => { +export const GroupChannelTypingIndicatorBubble = () => { const { sbOptions } = useSendbirdChat(); const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); if (typingUsers.length === 0) return null; if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null; - if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has('bubble')) return null; + if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has(TypingIndicatorType.Bubble)) return null; return ( - + ); }; diff --git a/packages/uikit-react-native/src/constants.ts b/packages/uikit-react-native/src/constants.ts index aa12a84a8..fb26b3cf8 100644 --- a/packages/uikit-react-native/src/constants.ts +++ b/packages/uikit-react-native/src/constants.ts @@ -5,8 +5,3 @@ export const MESSAGE_FOCUS_ANIMATION_DELAY = 250; export const UNKNOWN_USER_ID = '##__USER_ID_IS_NOT_PROVIDED__##'; export const VOICE_MESSAGE_META_ARRAY_DURATION_KEY = 'KEY_VOICE_MESSAGE_DURATION'; export const VOICE_MESSAGE_META_ARRAY_MESSAGE_TYPE_KEY = 'KEY_INTERNAL_MESSAGE_TYPE'; - -export enum TypingIndicatorType { - Text = 'text', - Bubble = 'bubble', -} diff --git a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx index 241820855..d2bd0f0cb 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx @@ -5,6 +5,7 @@ import { Header, Icon, createStyleSheet, useHeaderStyle } from '@sendbird/uikit- import ChannelCover from '../../../components/ChannelCover'; import { useLocalization, useSendbirdChat } from '../../../hooks/useContext'; +import { TypingIndicatorType } from '../../../types'; import { GroupChannelContexts } from '../module/moduleContext'; import type { GroupChannelProps } from '../types'; @@ -24,7 +25,7 @@ const GroupChannelHeader = ({ if (!subtitle) return null; if (!sbOptions.uikit.groupChannel.channel.enableTypingIndicator) return null; - if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has('text')) return null; + if (!sbOptions.uikit.groupChannel.channel.typingIndicatorTypes.has(TypingIndicatorType.Text)) return null; return {subtitle}; }; diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx index d9af2c2c2..853d1d15d 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx @@ -16,7 +16,7 @@ import { } from '@sendbird/uikit-utils'; import GroupChannelMessageRenderer, { - GroupChannelMessageTypingBubble, + GroupChannelTypingIndicatorBubble, } from '../components/GroupChannelMessageRenderer'; import NewMessagesButton from '../components/NewMessagesButton'; import ScrollToBottomButton from '../components/ScrollToBottomButton'; @@ -130,7 +130,7 @@ const createGroupChannelFragment = (initModule?: Partial): G return ( {content} - {props.isFirstItem && !hasNext() && } + {props.isFirstItem && !hasNext() && } ); }); diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts index 1bb249df3..5af081e70 100644 --- a/packages/uikit-react-native/src/index.ts +++ b/packages/uikit-react-native/src/index.ts @@ -134,7 +134,6 @@ export { default as SendbirdUIKitContainer, SendbirdUIKit } from './containers/S export type { SendbirdUIKitContainerProps } from './containers/SendbirdUIKitContainer'; export { default as SBUError } from './libs/SBUError'; export { default as SBUUtils } from './libs/SBUUtils'; -export { TypingIndicatorType } from './constants'; export * from './types'; Logger.setLogLevel(__DEV__ ? 'warn' : 'none'); diff --git a/packages/uikit-react-native/src/types.ts b/packages/uikit-react-native/src/types.ts index 2c783ca2b..9a5d32de5 100644 --- a/packages/uikit-react-native/src/types.ts +++ b/packages/uikit-react-native/src/types.ts @@ -28,3 +28,8 @@ export type Range = { start: number; end: number; }; + +export enum TypingIndicatorType { + Text = 'text', + Bubble = 'bubble', +}