From 19a1ea72f88283b6b374a7a4707db22084097d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 7 Nov 2024 14:04:17 +0100 Subject: [PATCH 1/2] Add the Component Library to the app --- app/constants/screens.ts | 2 + app/context/theme/index.tsx | 4 + app/screens/component_library/button.cl.tsx | 65 ++++++++ app/screens/component_library/hooks.tsx | 107 +++++++++++++ app/screens/component_library/index.tsx | 145 ++++++++++++++++++ app/screens/component_library/utils.tsx | 81 ++++++++++ app/screens/index.tsx | 3 + .../advanced/{index.tsx => advanced.tsx} | 29 +++- app/screens/settings/advanced/index.ts | 18 +++ 9 files changed, 452 insertions(+), 2 deletions(-) create mode 100644 app/screens/component_library/button.cl.tsx create mode 100644 app/screens/component_library/hooks.tsx create mode 100644 app/screens/component_library/index.tsx create mode 100644 app/screens/component_library/utils.tsx rename app/screens/settings/advanced/{index.tsx => advanced.tsx} (79%) create mode 100644 app/screens/settings/advanced/index.ts diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 8b4496c958a..4e57d090e05 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -19,6 +19,7 @@ export const CONVERT_GM_TO_CHANNEL = 'ConvertGMToChannel'; export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage'; export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel'; export const CREATE_TEAM = 'CreateTeam'; +export const COMPONENT_LIBRARY = 'ComponentLibrary'; export const CUSTOM_STATUS = 'CustomStatus'; export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter'; export const EDIT_POST = 'EditPost'; @@ -95,6 +96,7 @@ export default { CHANNEL_NOTIFICATION_PREFERENCES, CODE, CONVERT_GM_TO_CHANNEL, + COMPONENT_LIBRARY, CREATE_DIRECT_MESSAGE, CREATE_OR_EDIT_CHANNEL, CREATE_TEAM, diff --git a/app/context/theme/index.tsx b/app/context/theme/index.tsx index 1567d3e6ae7..c01a096a183 100644 --- a/app/context/theme/index.tsx +++ b/app/context/theme/index.tsx @@ -76,6 +76,10 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => { return ({children}); }; +export const CustomThemeProvider = ({theme, children}: {theme: Theme; children: React.ReactNode}) => { + return ({children}); +}; + export function withTheme(Component: ComponentType): ComponentType { return function ThemeComponent(props) { return ( diff --git a/app/screens/component_library/button.cl.tsx b/app/screens/component_library/button.cl.tsx new file mode 100644 index 00000000000..1b924b4b215 --- /dev/null +++ b/app/screens/component_library/button.cl.tsx @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {Alert, View} from 'react-native'; + +import Button from '@components/button'; +import {useTheme} from '@context/theme'; + +import {useBooleanProp, useDropdownProp, useStringProp} from './hooks'; +import {buildComponent} from './utils'; + +const propPossibilities = {}; + +const buttonSizeValues = ['xs', 's', 'm', 'lg']; +const buttonEmphasisValues = ['primary', 'secondary', 'tertiary', 'link']; +const buttonTypeValues = ['default', 'destructive', 'inverted', 'disabled']; +const buttonStateValues = ['default', 'hover', 'active', 'focus']; + +const onPress = () => Alert.alert('Button pressed!'); + +const ButtonComponentLibrary = () => { + const theme = useTheme(); + const [text, textSelector] = useStringProp('text', 'Some text', false); + const [disabled, disabledSelector] = useBooleanProp('disabled', false); + const [buttonSize, buttonSizePosibilities, buttonSizeSelector] = useDropdownProp('size', 'm', buttonSizeValues, true); + const [buttonEmphasis, buttonEmphasisPosibilities, buttonEmphasisSelector] = useDropdownProp('emphasis', 'primary', buttonEmphasisValues, true); + const [buttonType, buttonTypePosibilities, buttonTypeSelector] = useDropdownProp('buttonType', 'default', buttonTypeValues, true); + const [buttonState, buttonStatePosibilities, buttonStateSelector] = useDropdownProp('buttonState', 'default', buttonStateValues, true); + + const components = useMemo( + () => buildComponent(Button, propPossibilities, [ + buttonSizePosibilities, + buttonEmphasisPosibilities, + buttonTypePosibilities, + buttonStatePosibilities, + ], [ + text, + disabled, + buttonSize, + buttonEmphasis, + buttonType, + buttonState, + { + theme, + onPress, + }, + ]), + [buttonEmphasis, buttonEmphasisPosibilities, buttonSize, buttonSizePosibilities, buttonState, buttonStatePosibilities, buttonType, buttonTypePosibilities, disabled, text, theme], + ); + + return ( + <> + {textSelector} + {disabledSelector} + {buttonSizeSelector} + {buttonEmphasisSelector} + {buttonTypeSelector} + {buttonStateSelector} + {components} + + ); +}; + +export default ButtonComponentLibrary; diff --git a/app/screens/component_library/hooks.tsx b/app/screens/component_library/hooks.tsx new file mode 100644 index 00000000000..a307fd27347 --- /dev/null +++ b/app/screens/component_library/hooks.tsx @@ -0,0 +1,107 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useMemo, useState} from 'react'; + +import AutocompleteSelector from '@components/autocomplete_selector'; +import BoolSetting from '@components/settings/bool_setting'; +import TextSetting from '@components/settings/text_setting'; + +type HookResult = [ + {[x: string]: T}, + JSX.Element, +] +export const useStringProp = ( + propName: string, + defaultValue: string, + isTextarea: boolean, +): HookResult => { + const [value, setValue] = useState(defaultValue); + const selector = useMemo(() => ( + + ), [value, propName, isTextarea]); + const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]); + + return [preparedProp, selector]; +}; + +export const useBooleanProp = ( + propName: string, + defaultValue: boolean, +): HookResult => { + const [value, setValue] = useState(defaultValue); + const selector = useMemo(() => ( + + ), [propName, value]); + const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]); + + return [preparedProp, selector]; +}; + +const ALL_OPTION = 'ALL'; +type DropdownHookResult = [ + {[x: string]: string} | undefined, + {[x: string]: string[]} | undefined, + JSX.Element, +]; +export const useDropdownProp = ( + propName: string, + defaultValue: string, + options: string[], + allowAll: boolean, +): DropdownHookResult => { + const [value, setValue] = useState(defaultValue); + const onChange = useCallback((v: SelectedDialogOption) => { + if (!v) { + setValue(defaultValue); + return; + } + if (Array.isArray(v)) { + setValue(v[0].value); + return; + } + + setValue(v.value); + }, [defaultValue]); + + const renderedOptions = useMemo(() => { + const toReturn: DialogOption[] = options.map((v) => ({ + value: v, + text: v, + })); + if (allowAll) { + toReturn.unshift({ + value: ALL_OPTION, + text: ALL_OPTION, + }); + } + return toReturn; + }, [options, allowAll]); + const selector = useMemo(() => ( + + ), [onChange, propName, renderedOptions, value]); + const preparedProp = useMemo(() => (value === ALL_OPTION ? undefined : ({[propName]: value})), [propName, value]); + const preparedPossibilities = useMemo(() => (value === ALL_OPTION ? ({[propName]: options}) : undefined), [propName, value, options]); + return [preparedProp, preparedPossibilities, selector]; +}; diff --git a/app/screens/component_library/index.tsx b/app/screens/component_library/index.tsx new file mode 100644 index 00000000000..c8bc88a0f0a --- /dev/null +++ b/app/screens/component_library/index.tsx @@ -0,0 +1,145 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useMemo, useState} from 'react'; +import {ScrollView, View, type StyleProp, type ViewStyle} from 'react-native'; + +import AutocompleteSelector from '@components/autocomplete_selector'; +import {Preferences} from '@constants'; +import {CustomThemeProvider} from '@context/theme'; +import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; +import {popTopScreen} from '@screens/navigation'; + +import ButtonComponentLibrary from './button.cl'; + +import type {AvailableScreens} from '@typings/screens/navigation'; + +const componentMap = { + Button: ButtonComponentLibrary, +}; + +type ComponentName = keyof typeof componentMap +const defaultComponent = Object.keys(componentMap)[0] as ComponentName; +const componentOptions = Object.keys(componentMap).map((v) => ({ + value: v, + text: v, +})); + +type ThemeName = keyof typeof Preferences.THEMES; +const defaultTheme = Object.keys(Preferences.THEMES)[0] as ThemeName; +const themeOptions = Object.keys(Preferences.THEMES).map((v) => ({ + value: v, + text: v, +})); + +type BackgroundType = 'center' | 'sidebar'; +const backgroundOptions = [{ + value: 'center', + text: 'Center channel', +}, { + value: 'sidebar', + text: 'Sidebar background', +}]; + +type Props = { + componentId: AvailableScreens; +}; + +const ComponentLibrary = ({componentId}: Props) => { + const [selectedComponent, setSelectedComponent] = useState(defaultComponent); + const onSelectComponent = useCallback((value: SelectedDialogOption) => { + if (!value) { + setSelectedComponent(defaultComponent); + return; + } + + if (Array.isArray(value)) { + setSelectedComponent(value[0].value as ComponentName); + return; + } + setSelectedComponent(value.value as ComponentName); + }, []); + + const [selectedTheme, setSelectedTheme] = useState(defaultTheme); + const onSelectTheme = useCallback((value: SelectedDialogOption) => { + if (!value) { + setSelectedTheme(defaultTheme); + return; + } + + if (Array.isArray(value)) { + setSelectedTheme(value[0].value as ThemeName); + return; + } + setSelectedTheme(value.value as ThemeName); + }, []); + + const [selectedBackground, setSelectedBackground] = useState('center'); + const onSelectBackground = useCallback((value: SelectedDialogOption) => { + if (!value) { + setSelectedBackground('center'); + return; + } + + if (Array.isArray(value)) { + setSelectedBackground(value[0].value as BackgroundType); + return; + } + setSelectedBackground(value.value as BackgroundType); + }, []); + + const backgroundStyle: StyleProp = useMemo(() => { + const theme = Preferences.THEMES[selectedTheme]; + switch (selectedBackground) { + case 'center': + return { + backgroundColor: theme.centerChannelBg, + }; + case 'sidebar': + default: + return { + backgroundColor: theme.sidebarBg, + }; + } + }, [selectedBackground, selectedTheme]); + + const close = useCallback(() => { + popTopScreen(componentId); + }, [componentId]); + + useAndroidHardwareBackHandler(componentId, close); + + const SelectedComponent = componentMap[selectedComponent]; + return ( + + + + + + + + + + + ); +}; + +export default ComponentLibrary; diff --git a/app/screens/component_library/utils.tsx b/app/screens/component_library/utils.tsx new file mode 100644 index 00000000000..dddb6ff21d2 --- /dev/null +++ b/app/screens/component_library/utils.tsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text} from 'react-native'; + +function buildPropsLists(inputPossibilities: {[x: string]: any[]}): Array<{[x: string]: any}> { + const keys = Object.keys(inputPossibilities); + if (!keys.length) { + return [{}]; + } + + const selectedKey = keys[0]; + const restPossibilities = {...inputPossibilities}; + delete restPossibilities[selectedKey]; + const subProps = buildPropsLists(restPossibilities); + const result: Array<{[x: string]: any}> = []; + inputPossibilities[selectedKey].forEach((v) => { + subProps.forEach((rest) => { + result.push({...rest, [selectedKey]: v}); + }); + }); + + return result; +} + +function buildPropString(inputProps: {[x: string]: any}) { + const propKeys = Object.keys(inputProps); + if (!propKeys.length) { + return undefined; + } + + const result = [(<>{'PROPS: '})]; + propKeys.forEach((v) => { + result.push((<>{v}{`: ${inputProps[v]}, `})); + }); + return {result}; +} + +export function buildComponent( + Component: React.ComponentType, + propPossibilities: {[x: string]: any[]}, + dropdownPossibilities: Array<{[x: string]: string[]} | undefined>, + setProps: Array<{[x: string]: any} | undefined>, +) { + const res: React.ReactNode[] = []; + let currentPropPossibilities = {...propPossibilities}; + dropdownPossibilities.forEach((v) => { + if (v) { + currentPropPossibilities = { + ...currentPropPossibilities, + ...v, + }; + } + }); + + const propsVariations = buildPropsLists(currentPropPossibilities); + let builtSetProps = {}; + setProps.forEach((v) => { + if (v) { + builtSetProps = { + ...builtSetProps, + ...v, + }; + } + }); + propsVariations.forEach((v) => { + const propString = buildPropString(v); + res.push( + <> + {Boolean(propString) && {propString}} + + , + ); + }); + return res; +} diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 839f539114c..a33b4317c51 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -99,6 +99,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { case Screens.CREATE_OR_EDIT_CHANNEL: screen = withServerDatabase(require('@screens/create_or_edit_channel').default); break; + case Screens.COMPONENT_LIBRARY: + screen = withServerDatabase(require('@screens/component_library').default); + break; case Screens.CUSTOM_STATUS: screen = withServerDatabase(require('@screens/custom_status').default); break; diff --git a/app/screens/settings/advanced/index.tsx b/app/screens/settings/advanced/advanced.tsx similarity index 79% rename from app/screens/settings/advanced/index.tsx rename to app/screens/settings/advanced/advanced.tsx index 2a0d44a2d21..dc4421969b4 100644 --- a/app/screens/settings/advanced/index.tsx +++ b/app/screens/settings/advanced/advanced.tsx @@ -8,10 +8,11 @@ import {Alert, TouchableOpacity} from 'react-native'; import SettingContainer from '@components/settings/container'; import SettingOption from '@components/settings/option'; import SettingSeparator from '@components/settings/separator'; +import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; -import {popTopScreen} from '@screens/navigation'; +import {goToScreen, popTopScreen} from '@screens/navigation'; import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file'; import {preventDoubleTap} from '@utils/tap'; import {makeStyleSheetFromTheme} from '@utils/theme'; @@ -32,8 +33,12 @@ const EMPTY_FILES: FileInfo[] = []; type AdvancedSettingsProps = { componentId: AvailableScreens; + isDevMode: boolean; }; -const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => { +const AdvancedSettings = ({ + componentId, + isDevMode, +}: AdvancedSettingsProps) => { const theme = useTheme(); const intl = useIntl(); const serverUrl = useServerUrl(); @@ -77,6 +82,13 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => { } }); + const onPressComponentLibrary = () => { + const screen = Screens.COMPONENT_LIBRARY; + const title = intl.formatMessage({id: 'settings.advanced_settings.component_library', defaultMessage: 'Component library'}); + + goToScreen(screen, title); + }; + useEffect(() => { getAllCachedFiles(); }, []); @@ -107,6 +119,19 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => { /> + {isDevMode && ( + + + + + )} ); }; diff --git a/app/screens/settings/advanced/index.ts b/app/screens/settings/advanced/index.ts new file mode 100644 index 00000000000..2769077cf7a --- /dev/null +++ b/app/screens/settings/advanced/index.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase, withObservables} from '@nozbe/watermelondb/react'; + +import {observeConfigBooleanValue} from '@queries/servers/system'; + +import AdvancedSettings from './advanced'; + +import type {WithDatabaseArgs} from '@typings/database/database'; + +const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { + return { + isDevMode: observeConfigBooleanValue(database, 'EnableDeveloper', false), + }; +}); + +export default withDatabase(enhanced(AdvancedSettings)); From 219984ea6d33f131da1c09d7d5dba265955a52d0 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Sat, 9 Nov 2024 08:03:08 +0800 Subject: [PATCH 2/2] fix i18n --- assets/base/i18n/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index befafcfcd35..577fd9caf90 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1056,7 +1056,9 @@ "settings.about.server.version.title": "Server Version:", "settings.about.server.version.value": "{version} (Build {buildNumber})", "settings.advanced_settings": "Advanced Settings", + "settings.advanced_settings.component_library": "Component library", "settings.advanced.cancel": "Cancel", + "settings.advanced.component_library": "Component library", "settings.advanced.delete": "Delete", "settings.advanced.delete_data": "Delete local files", "settings.advanced.delete_message.confirmation": "\nThis will delete all files downloaded through the app for this server. Please confirm to proceed.\n",