From 08774638ba0e283f6da2c63545be746746b64058 Mon Sep 17 00:00:00 2001 From: bang9 Date: Fri, 21 Oct 2022 18:44:09 +0900 Subject: [PATCH] feat(uikit): added mini profile card --- .../src/ui/OutlinedButton/index.tsx | 7 +- .../src/ui/ProfileCard/index.tsx | 12 +- .../MessageRenderer/MessageIncomingAvatar.tsx | 11 +- .../src/components/UserActionBar.tsx | 14 +- .../src/containers/SendbirdUIKitContainer.tsx | 32 ++++- .../{Localization.tsx => LocalizationCtx.tsx} | 0 ...formService.tsx => PlatformServiceCtx.tsx} | 0 .../src/contexts/ProfileCardCtx.tsx | 122 ++++++++++++++++++ .../{SendbirdChat.tsx => SendbirdChatCtx.tsx} | 0 .../createGroupChannelMembersFragment.tsx | 50 +++---- .../src/hooks/useContext.ts | 13 +- packages/uikit-react-native/src/index.ts | 9 +- .../src/localization/StringSet.type.ts | 12 ++ packages/uikit-utils/src/index.ts | 1 + sample/package.json | 2 +- sample/src/App.tsx | 12 +- yarn.lock | 5 + 17 files changed, 248 insertions(+), 54 deletions(-) rename packages/uikit-react-native/src/contexts/{Localization.tsx => LocalizationCtx.tsx} (100%) rename packages/uikit-react-native/src/contexts/{PlatformService.tsx => PlatformServiceCtx.tsx} (100%) create mode 100644 packages/uikit-react-native/src/contexts/ProfileCardCtx.tsx rename packages/uikit-react-native/src/contexts/{SendbirdChat.tsx => SendbirdChatCtx.tsx} (100%) diff --git a/packages/uikit-react-native-foundation/src/ui/OutlinedButton/index.tsx b/packages/uikit-react-native-foundation/src/ui/OutlinedButton/index.tsx index c4651f85b..25f1097dd 100644 --- a/packages/uikit-react-native-foundation/src/ui/OutlinedButton/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/OutlinedButton/index.tsx @@ -11,10 +11,13 @@ type OutlinedButtonProps = { onPress?: () => void; }; -const OutlinedButton = ({ children, containerStyle }: OutlinedButtonProps) => { +const OutlinedButton = ({ children, onPress, containerStyle }: OutlinedButtonProps) => { const { colors } = useUIKitTheme(); return ( - + {children} diff --git a/packages/uikit-react-native-foundation/src/ui/ProfileCard/index.tsx b/packages/uikit-react-native-foundation/src/ui/ProfileCard/index.tsx index 8c80aa8fd..4cd8b09a6 100644 --- a/packages/uikit-react-native-foundation/src/ui/ProfileCard/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/ProfileCard/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View } from 'react-native'; +import { StyleProp, View, ViewStyle } from 'react-native'; import Divider from '../../components/Divider'; import Text from '../../components/Text'; @@ -15,17 +15,19 @@ type Props = { bodyLabel: string; body: string; + + containerStyle?: StyleProp; }; -const ProfileCard = ({ uri, username, bodyLabel, body, button }: Props) => { +const ProfileCard = ({ uri, username, bodyLabel, body, button, containerStyle }: Props) => { const { colors } = useUIKitTheme(); const color = colors.ui.profileCard.default.none; return ( - + - + {username} @@ -35,7 +37,7 @@ const ProfileCard = ({ uri, username, bodyLabel, body, button }: Props) => { {bodyLabel} - + {body} diff --git a/packages/uikit-react-native/src/components/MessageRenderer/MessageIncomingAvatar.tsx b/packages/uikit-react-native/src/components/MessageRenderer/MessageIncomingAvatar.tsx index 552fbac37..a0f47ddc9 100644 --- a/packages/uikit-react-native/src/components/MessageRenderer/MessageIncomingAvatar.tsx +++ b/packages/uikit-react-native/src/components/MessageRenderer/MessageIncomingAvatar.tsx @@ -1,18 +1,25 @@ import React from 'react'; -import { View } from 'react-native'; +import { Pressable, View } from 'react-native'; import { Avatar, createStyleSheet } from '@sendbird/uikit-react-native-foundation'; import type { SendbirdMessage } from '@sendbird/uikit-utils'; +import { useProfileCard } from '../../hooks/useContext'; + type Props = { message: SendbirdMessage; grouping: boolean; }; const MessageIncomingAvatar = ({ message, grouping }: Props) => { + const { show } = useProfileCard(); if (grouping) return ; return ( - {(message.isFileMessage() || message.isUserMessage()) && } + {(message.isFileMessage() || message.isUserMessage()) && ( + show(message.sender)}> + + + )} ); }; diff --git a/packages/uikit-react-native/src/components/UserActionBar.tsx b/packages/uikit-react-native/src/components/UserActionBar.tsx index e62628c8f..58fa82bb5 100644 --- a/packages/uikit-react-native/src/components/UserActionBar.tsx +++ b/packages/uikit-react-native/src/components/UserActionBar.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { TouchableOpacity, View } from 'react-native'; +import { Pressable, TouchableOpacity, View } from 'react-native'; +import type { GestureResponderEvent } from 'react-native'; import { Avatar, Icon, Text, createStyleSheet, useUIKitTheme } from '@sendbird/uikit-react-native-foundation'; import { conditionChaining } from '@sendbird/uikit-utils'; @@ -8,18 +9,23 @@ type Props = { uri: string; name: string; label?: string; + muted: boolean; disabled: boolean; - onPressActionMenu?: () => void; + + onPressActionMenu?: (ev: GestureResponderEvent) => void; + onPressAvatar?: (ev: GestureResponderEvent) => void; }; -const UserActionBar = ({ muted, uri, name, disabled, onPressActionMenu, label }: Props) => { +const UserActionBar = ({ muted, uri, name, disabled, label, onPressActionMenu, onPressAvatar }: Props) => { const { colors } = useUIKitTheme(); const iconColor = conditionChaining([disabled], [colors.onBackground04, colors.onBackground01]); return ( - + + + {name} diff --git a/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx b/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx index 76b416ae1..80166c1a0 100644 --- a/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx +++ b/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx @@ -14,11 +14,18 @@ import { ToastProvider, UIKitThemeProvider, } from '@sendbird/uikit-react-native-foundation'; -import type { SendbirdChatSDK } from '@sendbird/uikit-utils'; - -import { LocalizationProvider } from '../contexts/Localization'; -import { PlatformServiceProvider } from '../contexts/PlatformService'; -import { SendbirdChatProvider } from '../contexts/SendbirdChat'; +import type { + SendbirdChatSDK, + SendbirdGroupChannel, + SendbirdGroupChannelCreateParams, + SendbirdMember, + SendbirdUser, +} from '@sendbird/uikit-utils'; + +import { LocalizationProvider } from '../contexts/LocalizationCtx'; +import { PlatformServiceProvider } from '../contexts/PlatformServiceCtx'; +import { ProfileCardProvider } from '../contexts/ProfileCardCtx'; +import { SendbirdChatProvider } from '../contexts/SendbirdChatCtx'; import { useLocalization } from '../hooks/useContext'; import InternalLocalCacheStorage from '../libs/InternalLocalCacheStorage'; import StringSetEn from '../localization/StringSet.en'; @@ -69,6 +76,13 @@ export type SendbirdUIKitContainerProps = React.PropsWithChildren<{ toast?: { dismissTimeout?: number; }; + profileCard?: { + onCreateChannel: (channel: SendbirdGroupChannel) => void; + onBeforeCreateChannel?: ( + channelParams: SendbirdGroupChannelCreateParams, + users: SendbirdUser[] | SendbirdMember[], + ) => SendbirdGroupChannelCreateParams | Promise; + }; errorBoundary?: { onError?: (props: ErrorBoundaryProps) => void; ErrorInfoComponent?: (props: ErrorBoundaryProps) => JSX.Element; @@ -83,6 +97,7 @@ const SendbirdUIKitContainer = ({ localization, styles, toast, + profileCard, errorBoundary, }: SendbirdUIKitContainerProps) => { const unsubscribes = useRef<(() => void)[]>([]).current; @@ -163,7 +178,12 @@ const SendbirdUIKitContainer = ({ > - {children} + + {children} + diff --git a/packages/uikit-react-native/src/contexts/Localization.tsx b/packages/uikit-react-native/src/contexts/LocalizationCtx.tsx similarity index 100% rename from packages/uikit-react-native/src/contexts/Localization.tsx rename to packages/uikit-react-native/src/contexts/LocalizationCtx.tsx diff --git a/packages/uikit-react-native/src/contexts/PlatformService.tsx b/packages/uikit-react-native/src/contexts/PlatformServiceCtx.tsx similarity index 100% rename from packages/uikit-react-native/src/contexts/PlatformService.tsx rename to packages/uikit-react-native/src/contexts/PlatformServiceCtx.tsx diff --git a/packages/uikit-react-native/src/contexts/ProfileCardCtx.tsx b/packages/uikit-react-native/src/contexts/ProfileCardCtx.tsx new file mode 100644 index 000000000..f871732f7 --- /dev/null +++ b/packages/uikit-react-native/src/contexts/ProfileCardCtx.tsx @@ -0,0 +1,122 @@ +import React, { useContext, useState } from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { Modal, OutlinedButton, ProfileCard, createStyleSheet } from '@sendbird/uikit-react-native-foundation'; +import type { + SendbirdGroupChannel, + SendbirdGroupChannelCreateParams, + SendbirdMember, + SendbirdUser, +} from '@sendbird/uikit-utils'; +import { Logger, PASS, useIIFE } from '@sendbird/uikit-utils'; + +import { LocalizationContext } from '../contexts/LocalizationCtx'; +import { SendbirdChatContext } from '../contexts/SendbirdChatCtx'; + +type OnCreateChannel = (channel: SendbirdGroupChannel) => void; +type OnBeforeCreateChannel = ( + channelParams: SendbirdGroupChannelCreateParams, + users: SendbirdUser[] | SendbirdMember[], +) => SendbirdGroupChannelCreateParams | Promise; + +export type ProfileCardContextType = { + show(user: SendbirdUser | SendbirdMember): void; + hide(): void; +}; + +type Props = React.PropsWithChildren<{ + onCreateChannel?: OnCreateChannel; + onBeforeCreateChannel?: OnBeforeCreateChannel; +}>; + +export const ProfileCardContext = React.createContext(null); +export const ProfileCardProvider = ({ children, onCreateChannel, onBeforeCreateChannel = PASS }: Props) => { + const chatContext = useContext(SendbirdChatContext); + const localizationContext = useContext(LocalizationContext); + const { bottom, left, right } = useSafeAreaInsets(); + + const [user, setUser] = useState(); + const [visible, setVisible] = useState(false); + + const show: ProfileCardContextType['show'] = (user) => { + setUser(user); + setVisible(true); + }; + + const hide: ProfileCardContextType['hide'] = () => { + setVisible(false); + }; + + if (!chatContext) throw new Error('SendbirdChatContext is not provided'); + if (!localizationContext) throw new Error('LocalizationContext is not provided'); + + const isMe = chatContext.currentUser && user?.userId === chatContext.currentUser.userId; + const messageToUser = async () => { + if (user) { + const params: SendbirdGroupChannelCreateParams = { + invitedUserIds: [user.userId], + name: '', + coverUrl: '', + isDistinct: false, + }; + + if (chatContext.currentUser) params.operatorUserIds = [chatContext.currentUser.userId]; + const processedParams = await onBeforeCreateChannel(params, [user]); + + hide(); + const channel = await chatContext.sdk.groupChannel.createChannel(processedParams); + + if (onCreateChannel) { + onCreateChannel(channel); + } else { + Logger.warn( + 'Please set `onCreateChannel` before message to user from profile card, see `profileCard` prop in the `SendbirdUIKitContainer` props', + ); + } + } + }; + + return ( + + {children} + setUser(undefined)} + visible={visible && Boolean(user)} + backgroundStyle={styles.modal} + > + {user && ( + { + if (isMe) return undefined; + return ( + + {localizationContext.STRINGS.PROFILE_CARD.BUTTON_MESSAGE} + + ); + })} + /> + )} + + + ); +}; + +const styles = createStyleSheet({ + modal: { + justifyContent: 'flex-end', + }, + profileCardContainer: { + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + }, +}); diff --git a/packages/uikit-react-native/src/contexts/SendbirdChat.tsx b/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx similarity index 100% rename from packages/uikit-react-native/src/contexts/SendbirdChat.tsx rename to packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx index 2dea131ba..58fb53bb3 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelMembersFragment.tsx @@ -1,15 +1,15 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { useActiveGroupChannel, useChannelHandler } from '@sendbird/uikit-chat-hooks'; import { Icon } from '@sendbird/uikit-react-native-foundation'; import type { SendbirdMember } from '@sendbird/uikit-utils'; -import { useForceUpdate, useUniqId } from '@sendbird/uikit-utils'; +import { useForceUpdate, useFreshCallback, useUniqId } from '@sendbird/uikit-utils'; import UserActionBar from '../components/UserActionBar'; import type { GroupChannelMembersFragment } from '../domain/groupChannelUserList/types'; import createUserListModule from '../domain/userList/module/createUserListModule'; import type { UserListModule } from '../domain/userList/types'; -import { useLocalization, useSendbirdChat } from '../hooks/useContext'; +import { useLocalization, useProfileCard, useSendbirdChat } from '../hooks/useContext'; const noop = () => ''; const name = 'createGroupChannelMembersFragment'; @@ -21,10 +21,12 @@ const createGroupChannelMembersFragment = ( return ({ channel, onPressHeaderLeft, onPressHeaderRight, renderUser }) => { const uniqId = useUniqId(name); const forceUpdate = useForceUpdate(); - const { sdk, currentUser } = useSendbirdChat(); - const { activeChannel } = useActiveGroupChannel(sdk, channel); const { STRINGS } = useLocalization(); + const { sdk, currentUser } = useSendbirdChat(); + const { show } = useProfileCard(); + + const { activeChannel } = useActiveGroupChannel(sdk, channel); useChannelHandler(sdk, `${name}_${uniqId}`, { // Note: Removed from v4 @@ -63,27 +65,25 @@ const createGroupChannelMembersFragment = ( }, }); - const _renderUser: NonNullable = useCallback( - (user, selectedUsers, setSelectedUsers) => { - if (renderUser) return renderUser(user, selectedUsers, setSelectedUsers); + const _renderUser: NonNullable = useFreshCallback((user, selectedUsers, setSelectedUsers) => { + if (renderUser) return renderUser(user, selectedUsers, setSelectedUsers); - return ( - - ); - }, - [renderUser], - ); + return ( + show(user)} + /> + ); + }); return ( diff --git a/packages/uikit-react-native/src/hooks/useContext.ts b/packages/uikit-react-native/src/hooks/useContext.ts index 87fa39e5e..2f8516b22 100644 --- a/packages/uikit-react-native/src/hooks/useContext.ts +++ b/packages/uikit-react-native/src/hooks/useContext.ts @@ -1,8 +1,9 @@ import { useContext } from 'react'; -import { LocalizationContext } from '../contexts/Localization'; -import { PlatformServiceContext } from '../contexts/PlatformService'; -import { SendbirdChatContext } from '../contexts/SendbirdChat'; +import { LocalizationContext } from '../contexts/LocalizationCtx'; +import { PlatformServiceContext } from '../contexts/PlatformServiceCtx'; +import { ProfileCardContext } from '../contexts/ProfileCardCtx'; +import { SendbirdChatContext } from '../contexts/SendbirdChatCtx'; export const useLocalization = () => { const value = useContext(LocalizationContext); @@ -21,3 +22,9 @@ export const useSendbirdChat = () => { if (!value) throw new Error('SendbirdChatContext is not provided'); return value; }; + +export const useProfileCard = () => { + const value = useContext(ProfileCardContext); + if (!value) throw new Error('ProfileCardContext is not provided'); + return value; +}; diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts index a7f494521..56be65422 100644 --- a/packages/uikit-react-native/src/index.ts +++ b/packages/uikit-react-native/src/index.ts @@ -24,14 +24,15 @@ export { default as createGroupChannelListFragment } from './fragments/createGro export { default as createGroupChannelMembersFragment } from './fragments/createGroupChannelMembersFragment'; /** Context **/ -export { SendbirdChatContext, SendbirdChatProvider } from './contexts/SendbirdChat'; -export { PlatformServiceContext, PlatformServiceProvider } from './contexts/PlatformService'; -export { LocalizationContext, LocalizationProvider } from './contexts/Localization'; +export { SendbirdChatContext, SendbirdChatProvider } from './contexts/SendbirdChatCtx'; +export { PlatformServiceContext, PlatformServiceProvider } from './contexts/PlatformServiceCtx'; +export { ProfileCardContext, ProfileCardProvider } from './contexts/ProfileCardCtx'; +export { LocalizationContext, LocalizationProvider } from './contexts/LocalizationCtx'; /** Hooks **/ export { default as useConnection } from './hooks/useConnection'; export { default as usePushTokenRegistration } from './hooks/usePushTokenRegistration'; -export { useLocalization, usePlatformService, useSendbirdChat } from './hooks/useContext'; +export { useLocalization, usePlatformService, useSendbirdChat, useProfileCard } from './hooks/useContext'; /** Localization **/ export { default as StringSetEn } from './localization/StringSet.en'; diff --git a/packages/uikit-react-native/src/localization/StringSet.type.ts b/packages/uikit-react-native/src/localization/StringSet.type.ts index 986a05181..9af3a114d 100644 --- a/packages/uikit-react-native/src/localization/StringSet.type.ts +++ b/packages/uikit-react-native/src/localization/StringSet.type.ts @@ -4,6 +4,7 @@ import type { PartialDeep, SendbirdFileMessage, SendbirdGroupChannel, + SendbirdMember, SendbirdMessage, SendbirdUser, } from '@sendbird/uikit-utils'; @@ -174,6 +175,11 @@ export interface StringSet { TURN_OFF_NOTIFICATIONS_ERROR: string; LEAVE_CHANNEL_ERROR: string; }; + PROFILE_CARD: { + BUTTON_MESSAGE: string; + BODY_LABEL: string; + BODY: (user: SendbirdUser | SendbirdMember) => string; + }; } type StringSetCreateOptions = { @@ -348,5 +354,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp LEAVE_CHANNEL_ERROR: "Couldn't leave channel.", ...overrides?.TOAST, }, + PROFILE_CARD: { + BUTTON_MESSAGE: 'Message', + BODY_LABEL: 'User ID', + BODY: (user) => user.userId, + ...overrides?.PROFILE_CARD, + }, }; }; diff --git a/packages/uikit-utils/src/index.ts b/packages/uikit-utils/src/index.ts index c7efb3703..cb677a5fc 100644 --- a/packages/uikit-utils/src/index.ts +++ b/packages/uikit-utils/src/index.ts @@ -28,6 +28,7 @@ export const NOOP: () => void = () => void 0; export const ASYNC_NOOP = async () => void 0; export const PASS = (val: T) => val; export const toMegabyte = (byte: number) => byte / 1024 / 1024; +export const isFunction = (param?: T): param is NonNullable => typeof param === 'function'; export type { FilterByValueType, diff --git a/sample/package.json b/sample/package.json index 7f56f5c9a..c1e364287 100644 --- a/sample/package.json +++ b/sample/package.json @@ -28,7 +28,7 @@ "@react-navigation/bottom-tabs": "^6.2.0", "@react-navigation/native": "^6.0.6", "@react-navigation/native-stack": "^6.7.0", - "@sendbird/chat": "^4.0.13", + "@sendbird/chat": "^4.1.0", "@storybook/addon-actions": "^6.4.19", "@storybook/addon-controls": "^6.4.19", "@storybook/addon-ondevice-actions": "^6.0.1-alpha.7", diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 71e44ddd1..7fd5ac5db 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -18,7 +18,7 @@ import { SetSendbirdSDK, } from './factory'; import useAppearance from './hooks/useAppearance'; -import { Routes, navigationRef } from './libs/navigation'; +import { Routes, navigationActions, navigationRef } from './libs/navigation'; import { onForegroundAndroid, onForegroundIOS } from './libs/notification'; import { ErrorInfoScreen, @@ -62,6 +62,13 @@ const App = () => { statusBarTranslucent: GetTranslucent(), }} errorBoundary={{ ErrorInfoComponent: ErrorInfoScreen }} + profileCard={{ + onCreateChannel: (channel) => { + navigationActions.push(Routes.GroupChannel, { + serializedChannel: channel.serialize(), + }); + }, + }} > @@ -81,10 +88,11 @@ const Navigations = () => { }, []); useEffect(() => { - AppState.addEventListener('change', async () => { + const { remove } = AppState.addEventListener('change', async () => { const count = await sdk.groupChannel.getTotalUnreadMessageCount(); Notifee.setBadgeCount(count); }); + return () => remove(); }, []); return ( diff --git a/yarn.lock b/yarn.lock index 60461eaa0..3855aca6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3196,6 +3196,11 @@ resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.0.13.tgz#21b737eecf9053ca87e0db5713cd17446eb8f302" integrity sha512-vmIJldcVa8m5Wd9YBhbrfAX9w+N5iDAZnkeBnYQs5I4ov9txIs02+GqpobSWi77ywx/dgPVLm1+7nIkyuiCTLw== +"@sendbird/chat@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.1.0.tgz#07994edaa5aa498b78b2758d0f8628ab4edefa79" + integrity sha512-LL8wA55xh/32SISmyIX3pNm//a7ANlum5rUA2iWzhLHN52yXNqeBCIqEXMakGDm7hkIbaRLTzDWAESXuy/a78w== + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"