From e68d9a6f3747930c18b7cb5d8c4c8ec7c7915f47 Mon Sep 17 00:00:00 2001 From: bang9 Date: Mon, 7 Mar 2022 19:34:16 +0900 Subject: [PATCH] feat(foundation): added Placeholder component --- .../src/localization/label.type.ts | 25 +++++++ .../src/index.tsx | 2 + .../src/theme/DarkUIKitTheme.ts | 8 +++ .../src/theme/LightUIKitTheme.ts | 16 +++-- .../src/types.ts | 6 +- .../src/ui/ActionMenu/index.tsx | 7 +- .../src/ui/Button/index.tsx | 4 +- .../src/ui/LoadingSpinner/index.tsx | 46 ++++++++++++ .../src/ui/Placeholder/index.tsx | 70 +++++++++++++++++++ packages/uikit-react-native/src/index.tsx | 1 + .../src/ui/TypedPlaceholder/index.tsx | 42 +++++++++++ sample/.storybook/preview.js | 6 +- sample/.storybook/storybook.requires.js | 1 + sample/src/App.tsx | 4 +- sample/stories/Dialog.stories.tsx | 5 +- sample/stories/Placeholder.stories.tsx | 36 ++++++++++ 16 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 packages/uikit-react-native-foundation/src/ui/LoadingSpinner/index.tsx create mode 100644 packages/uikit-react-native-foundation/src/ui/Placeholder/index.tsx create mode 100644 packages/uikit-react-native/src/ui/TypedPlaceholder/index.tsx create mode 100644 sample/stories/Placeholder.stories.tsx diff --git a/packages/uikit-react-native-core/src/localization/label.type.ts b/packages/uikit-react-native-core/src/localization/label.type.ts index cdfe21403..c40faee92 100644 --- a/packages/uikit-react-native-core/src/localization/label.type.ts +++ b/packages/uikit-react-native-core/src/localization/label.type.ts @@ -46,6 +46,17 @@ export interface LabelSet { /** @domain InviteMembers > Header > Right */ HEADER_RIGHT: (params: { selectedUsers: Array }) => string; }; + PLACEHOLDER: { + NO_BANNED_MEMBERS: string; + NO_CHANNELS: string; + NO_MESSAGES: string; + NO_MUTED_MEMBERS: string; + NO_RESULTS_FOUND: string; + ERROR_SOMETHING_IS_WRONG: { + MESSAGE: string; + RETRY_LABEL: string; + }; + }; } type LabelCreateOptions = { @@ -77,6 +88,7 @@ export const createBaseLabel = ({ dateLocale, overrides }: LabelCreateOptions): return 'Turn off notifications'; }, MENU_LEAVE_CHANNEL: 'Leave channel', + ...overrides?.GROUP_CHANNEL_LIST?.CHANNEL_MENU, }, }, INVITE_MEMBERS: { @@ -88,4 +100,17 @@ export const createBaseLabel = ({ dateLocale, overrides }: LabelCreateOptions): }, ...overrides?.INVITE_MEMBERS, }, + PLACEHOLDER: { + NO_BANNED_MEMBERS: 'No banned members', + NO_CHANNELS: 'There are no channels', + NO_MESSAGES: 'There are no messages', + NO_MUTED_MEMBERS: 'No muted members', + NO_RESULTS_FOUND: 'No results found', + ...overrides?.PLACEHOLDER, + ERROR_SOMETHING_IS_WRONG: { + MESSAGE: 'Something is wrong', + RETRY_LABEL: 'Retry', + ...overrides?.PLACEHOLDER?.ERROR_SOMETHING_IS_WRONG, + }, + }, }); diff --git a/packages/uikit-react-native-foundation/src/index.tsx b/packages/uikit-react-native-foundation/src/index.tsx index 080b55634..0f420ed3c 100644 --- a/packages/uikit-react-native-foundation/src/index.tsx +++ b/packages/uikit-react-native-foundation/src/index.tsx @@ -16,7 +16,9 @@ export { DialogProvider, useActionMenu, useAlert, usePrompt } from './ui/Dialog' export { default as Divider } from './ui/Divider'; export { default as Header } from './ui/Header'; export { default as Icon } from './ui/Icon'; +export { default as LoadingSpinner } from './ui/LoadingSpinner'; export { default as Modal } from './ui/Modal'; +export { default as Placeholder } from './ui/Placeholder'; export { default as Prompt } from './ui/Prompt'; export { default as Switch } from './ui/Switch'; export { default as Text } from './ui/Text'; diff --git a/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts b/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts index 28267c7f7..98cf54c1e 100644 --- a/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts +++ b/packages/uikit-react-native-foundation/src/theme/DarkUIKitTheme.ts @@ -112,6 +112,14 @@ const DarkUIKitTheme: UIKitTheme = { }, }, }, + placeholder: { + default: { + none: { + content: Palette.onBackgroundDark03, + highlight: Palette.primary200, + }, + }, + }, }, }, }; diff --git a/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts b/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts index ad29433c1..6fbf9f039 100644 --- a/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts +++ b/packages/uikit-react-native-foundation/src/theme/LightUIKitTheme.ts @@ -1,7 +1,7 @@ -import createAppearanceHelper from '../styles/appearanceHelper'; -import type { UIKitTheme } from '../types'; -import Palette from './Palette'; -import { defaultTypography } from './Typography'; +import createAppearanceHelper from "../styles/appearanceHelper"; +import type { UIKitTheme } from "../types"; +import Palette from "./Palette"; +import { defaultTypography } from "./Typography"; const appearance = 'light'; const LightUIKitTheme: UIKitTheme = { @@ -112,6 +112,14 @@ const LightUIKitTheme: UIKitTheme = { }, }, }, + placeholder: { + default: { + none: { + content: Palette.onBackgroundLight03, + highlight: Palette.primary300, + }, + }, + }, }, }, }; diff --git a/packages/uikit-react-native-foundation/src/types.ts b/packages/uikit-react-native-foundation/src/types.ts index 968a28be6..c68add59f 100644 --- a/packages/uikit-react-native-foundation/src/types.ts +++ b/packages/uikit-react-native-foundation/src/types.ts @@ -28,7 +28,7 @@ export interface UIKitTheme extends AppearanceHelper { typography: Typography; } -type Component = 'Header' | 'Button' | 'Dialog' | 'Input' | 'Badge'; +type Component = 'Header' | 'Button' | 'Dialog' | 'Input' | 'Badge' | 'Placeholder'; type GetColorTree< Tree extends { Variant: { @@ -50,6 +50,7 @@ export type ComponentColorTree = GetColorTree<{ Dialog: 'default'; Input: 'default' | 'underline'; Badge: 'default'; + Placeholder: 'default'; }; State: { Header: 'none'; @@ -57,6 +58,7 @@ export type ComponentColorTree = GetColorTree<{ Dialog: 'none'; Input: 'active' | 'disabled'; Badge: 'none'; + Placeholder: 'none'; }; ColorPart: { Header: 'background' | 'borderBottom'; @@ -64,6 +66,7 @@ export type ComponentColorTree = GetColorTree<{ Dialog: 'background' | 'text' | 'message' | 'highlight' | 'destructive'; Input: 'text' | 'placeholder' | 'background' | 'highlight'; Badge: 'text' | 'background'; + Placeholder: 'content' | 'highlight'; }; }>; type ComponentColors = { @@ -104,6 +107,7 @@ export type UIKitColors = { dialog: ComponentColors<'Dialog'>; input: ComponentColors<'Input'>; badge: ComponentColors<'Badge'>; + placeholder: ComponentColors<'Placeholder'>; }; }; diff --git a/packages/uikit-react-native-foundation/src/ui/ActionMenu/index.tsx b/packages/uikit-react-native-foundation/src/ui/ActionMenu/index.tsx index 2504d92c3..1fc9a9425 100644 --- a/packages/uikit-react-native-foundation/src/ui/ActionMenu/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/ActionMenu/index.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { ActivityIndicator, TouchableOpacity, View } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import { Logger } from '@sendbird/uikit-utils'; @@ -7,6 +7,7 @@ import createStyleSheet from '../../styles/createStyleSheet'; import useHeaderStyle from '../../styles/useHeaderStyle'; import useUIKitTheme from '../../theme/useUIKitTheme'; import DialogBox from '../Dialog/DialogBox'; +import LoadingSpinner from '../LoadingSpinner'; import Modal from '../Modal'; import Text from '../Text'; @@ -49,8 +50,8 @@ const ActionMenu: React.FC = ({ visible, onHide, onError, onDismiss, titl {title} {pending && ( - diff --git a/packages/uikit-react-native-foundation/src/ui/Button/index.tsx b/packages/uikit-react-native-foundation/src/ui/Button/index.tsx index 22cb6e4e4..76c972de3 100644 --- a/packages/uikit-react-native-foundation/src/ui/Button/index.tsx +++ b/packages/uikit-react-native-foundation/src/ui/Button/index.tsx @@ -48,7 +48,9 @@ const Button: React.FC = ({ return ( <> - {icon && } + {icon && ( + + )} {children} diff --git a/packages/uikit-react-native-foundation/src/ui/LoadingSpinner/index.tsx b/packages/uikit-react-native-foundation/src/ui/LoadingSpinner/index.tsx new file mode 100644 index 000000000..3b2d2bff4 --- /dev/null +++ b/packages/uikit-react-native-foundation/src/ui/LoadingSpinner/index.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, Easing, StyleProp, ViewStyle } from 'react-native'; + +import useUIKitTheme from '../../theme/useUIKitTheme'; +import Icon from '../Icon'; + +type Props = { + size?: number; + color?: string; + style?: StyleProp; +}; + +const LoadingSpinner: React.FC = ({ size = 24, color, style }) => { + const { colors } = useUIKitTheme(); + return ( + + + + ); +}; + +const useLoopAnimated = (duration: number, useNativeDriver = true) => { + const animated = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.loop( + Animated.timing(animated, { toValue: 1, duration, useNativeDriver, easing: Easing.inOut(Easing.linear) }), + { resetBeforeIteration: true }, + ).start(); + + return () => { + animated.stopAnimation(); + animated.setValue(0); + }; + }, []); + + return animated; +}; + +const Rotate: React.FC<{ style: StyleProp }> = ({ children, style }) => { + const loop = useLoopAnimated(1000); + const rotate = loop.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] }); + return {children}; +}; + +export default LoadingSpinner; diff --git a/packages/uikit-react-native-foundation/src/ui/Placeholder/index.tsx b/packages/uikit-react-native-foundation/src/ui/Placeholder/index.tsx new file mode 100644 index 000000000..e61488236 --- /dev/null +++ b/packages/uikit-react-native-foundation/src/ui/Placeholder/index.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { View } from 'react-native'; + +import createStyleSheet from '../../styles/createStyleSheet'; +import useUIKitTheme from '../../theme/useUIKitTheme'; +import Button from '../Button'; +import Icon from '../Icon'; +import LoadingSpinner from '../LoadingSpinner'; +import Text from '../Text'; + +type Props = { + loading?: boolean; + + icon: keyof typeof Icon.Assets; + message?: string; + errorRetryLabel?: string; + onPressRetry?: () => void; +}; + +const Placeholder: React.FC = ({ icon, loading = false, message = '', errorRetryLabel, onPressRetry }) => { + const { colors } = useUIKitTheme(); + + return ( + + {loading ? ( + + ) : ( + + )} + {Boolean(message) && !loading && ( + + {message} + + )} + {Boolean(errorRetryLabel) && !loading && ( + + )} + + ); +}; + +const styles = createStyleSheet({ + container: { + width: 200, + height: 100, + alignItems: 'center', + justifyContent: 'space-between', + }, + containerError: { + width: 200, + height: 148, + alignItems: 'center', + justifyContent: 'space-between', + }, + containerLoading: { + width: 200, + height: 100, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default Placeholder; diff --git a/packages/uikit-react-native/src/index.tsx b/packages/uikit-react-native/src/index.tsx index b2ecf0924..c62f9288d 100644 --- a/packages/uikit-react-native/src/index.tsx +++ b/packages/uikit-react-native/src/index.tsx @@ -1,5 +1,6 @@ /** UI **/ export { default as GroupChannelPreview } from './ui/GroupChannelPreview'; +export { default as TypedPlaceholder } from './ui/TypedPlaceholder'; /** Fragments **/ export { default as createGroupChannelListFragment } from './fragments/createGroupChannelListFragment'; diff --git a/packages/uikit-react-native/src/ui/TypedPlaceholder/index.tsx b/packages/uikit-react-native/src/ui/TypedPlaceholder/index.tsx new file mode 100644 index 000000000..5a93b2e6e --- /dev/null +++ b/packages/uikit-react-native/src/ui/TypedPlaceholder/index.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +import { useLocalization } from '@sendbird/uikit-react-native-core'; +import Placeholder from '@sendbird/uikit-react-native-foundation/src/ui/Placeholder'; + +type Props = { + type: + | 'no-banned-members' + | 'no-channels' + | 'no-messages' + | 'no-muted-members' + | 'no-results-found' + | 'error-wrong' + | 'loading'; +}; +const TypedPlaceholder: React.FC = ({ type }) => { + const { LABEL } = useLocalization(); + switch (type) { + case 'no-banned-members': + return ; + case 'no-channels': + return ; + case 'no-messages': + return ; + case 'no-muted-members': + return ; + case 'no-results-found': + return ; + case 'error-wrong': + return ( + + ); + case 'loading': + return ; + } +}; + +export default TypedPlaceholder; diff --git a/sample/.storybook/preview.js b/sample/.storybook/preview.js index 3441109f6..bb985a0da 100644 --- a/sample/.storybook/preview.js +++ b/sample/.storybook/preview.js @@ -1,6 +1,6 @@ import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds'; import React from 'react'; -import { View } from 'react-native'; +import { Appearance, View } from 'react-native'; import { Palette } from '@sendbird/uikit-react-native-foundation'; @@ -12,8 +12,8 @@ export const decorators = [ ]; export const parameters = { backgrounds: [ - { name: 'light', value: Palette.background50, default: true }, - { name: 'dark', value: Palette.background600 }, + { name: 'light', value: Palette.background50, default: Appearance.getColorScheme() === 'light' }, + { name: 'dark', value: Palette.background600, default: Appearance.getColorScheme() === 'dark' }, ], layout: 'fullscreen', options: { diff --git a/sample/.storybook/storybook.requires.js b/sample/.storybook/storybook.requires.js index 28ee25f5d..74d7c3864 100644 --- a/sample/.storybook/storybook.requires.js +++ b/sample/.storybook/storybook.requires.js @@ -34,6 +34,7 @@ const getStories = () => { require("../stories/Dialog.stories.tsx"), require("../stories/GroupChannelPreview.stories.tsx"), require("../stories/Icon.stories.tsx"), + require("../stories/Placeholder.stories.tsx"), require("../stories/Text.stories.tsx"), ]; }; diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 6a2aa990e..7e3479d90 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -16,7 +16,7 @@ import { Routes } from './hooks/useAppNavigation'; import useAppearance from './hooks/useAppearance'; import { GroupChannelTabs, HomeScreen, InviteMembersScreen, PaletteScreen, ThemeColorsScreen } from './screens'; -Platform.OS === 'android' && StatusBar.setTranslucent(false); +Platform.OS === 'android' && StatusBar.setTranslucent(true); const sdkInstance = new SendBird({ appId: APP_ID }) as SendbirdChatSDK; const filePicker = createFilePickerServiceNative(ImagePicker, Permissions); const RootStack = createNativeStackNavigator(); @@ -31,7 +31,7 @@ const App = () => { services={{ filePicker, notification: {} as any }} styles={{ theme: isLightTheme ? LightUIKitTheme : DarkUIKitTheme, - statusBarTranslucent: Platform.select({ ios: true, android: false }), + statusBarTranslucent: Platform.select({ ios: true, android: true }), }} > diff --git a/sample/stories/Dialog.stories.tsx b/sample/stories/Dialog.stories.tsx index 05e1654fe..ed38a4bf7 100644 --- a/sample/stories/Dialog.stories.tsx +++ b/sample/stories/Dialog.stories.tsx @@ -68,15 +68,14 @@ const WrappedActionMenu: React.FC = () => { }, }, { - title: 'Open menu 3 times after 1 sec', + title: 'Open menu 2 times after 2 sec', onPress: () => { return new Promise((resolve) => { setTimeout(() => { resolve(0); openMenu({ title: 'Menu1', menuItems: [{ title: 'Hello' }] }); openMenu({ title: 'Menu2', menuItems: [{ title: 'Hello' }] }); - openMenu({ title: 'Menu3', menuItems: [{ title: 'Hello' }] }); - }, 1000); + }, 2000); }); }, }, diff --git a/sample/stories/Placeholder.stories.tsx b/sample/stories/Placeholder.stories.tsx new file mode 100644 index 000000000..605fbec70 --- /dev/null +++ b/sample/stories/Placeholder.stories.tsx @@ -0,0 +1,36 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react-native'; +import React from 'react'; +import { ScrollView, View } from 'react-native'; + +import { TypedPlaceholder as TypedPlaceholderComponent } from '@sendbird/uikit-react-native'; +import { Placeholder as PlaceholderComponent } from '@sendbird/uikit-react-native-foundation'; + +const PlaceholderMeta: ComponentMeta = { + title: 'Placeholder', + component: PlaceholderComponent, + argTypes: {}, + args: {}, +}; + +export default PlaceholderMeta; + +type PlaceholderStory = ComponentStory; +export const Placeholder: PlaceholderStory = () => ( + + + +); + +export const TypedPlaceholder: PlaceholderStory = () => ( + + + + + + + + + + + +);