diff --git a/app/constants/screens.ts b/app/constants/screens.ts
index 23bc9ea89..93be7156e 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 DRAFT = 'Draft';
@@ -98,6 +99,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 1567d3e6a..c01a096a1 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 000000000..1b924b4b2
--- /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 000000000..a307fd273
--- /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 000000000..c8bc88a0f
--- /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 000000000..dddb6ff21
--- /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 540c80b15..c7a823c4b 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 2a0d44a2d..dc4421969 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 000000000..2769077cf
--- /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));
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index e685f71cd..cecd1dcf8 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1075,7 +1075,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",