MM-39711 - Gekidou - Theme functionality (#6327)

* added chevron to menu item component

* starting with the skeleton

* starting with the skeleton

* starting with the skeleton

* starting with the skeleton

* remove extra line

* tested on tablets

* some corrections

* corrections as per review

* starting with notification skeleton

* attached notification settings to navigation

* added auto responder

* update translation

* update snapshot

* updated snapshot

* correction after review

* removed unnecessary screen

* refactored

* updated the testIDs

* Update Package.resolved

* refactor

* removed Mattermost as default server name

* fix ts

* refactored settings constant

* display settings skeleton

- pending: query for allowed themes

* added 'allowedThemes' query

* added section item

* mention screen skeleton in place

* added section and sectionItem component

* added reply section to the mention screen

* update i18n

* rename screens properly

* update i18n

* Refactored to MentionSettings component

* Refactored to ReplySettings component

* style clean up

* textTransform uppercase

* rename Section/SectionItem to Block/BlockItem

* added mobile push notif screen - push status section

* adding text to those two components

* correction following review

* added mobile push notification section

* added mobile push notification thread section

* style fix

* code fix

* code fix

* added skeleton for auto responder

* code clean up

* display theme skeleton

* display theme skeleton

* now using selected theme

* clean up code

* showing custom theme

* setTheme implemented

* code clean up

* some corrections

* Gekidou - Replace BlockItem with OptionItem (#6352)

* Replaced BlockItem component with OptionItem

* ui fix

* corrections from PR review

* code clean up

* correction from PR review

* fix - SettingsDisplay was wrongly removed from @screen/index

* fix dependencies

* corrections from peer review
This commit is contained in:
Avinash Lingaloo
2022-06-10 14:28:35 +04:00
committed by GitHub
parent 6906a9f863
commit bb02b1178a
21 changed files with 690 additions and 284 deletions

View File

@@ -47,7 +47,7 @@ type SectionProps = {
disableFooter?: boolean;
disableHeader?: boolean;
footerText?: SectionText;
headerText: SectionText;
headerText?: SectionText;
containerStyles?: StyleProp<ViewStyle>;
headerStyles?: StyleProp<TextStyle>;
footerStyles?: StyleProp<TextStyle>;

View File

@@ -1,169 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactElement, useCallback, useMemo} from 'react';
import {StyleProp, Switch, Text, TextStyle, TouchableOpacity, View, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const ActionTypes = {
ARROW: 'arrow',
DEFAULT: 'default',
TOGGLE: 'toggle',
SELECT: 'select',
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flexDirection: 'row',
alignItems: 'center',
},
singleContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 45,
},
doubleContainer: {
flex: 1,
flexDirection: 'column',
height: 69,
justifyContent: 'center',
},
label: {
color: theme.centerChannelColor,
...typography('Body', 200, 'SemiBold'),
marginLeft: 9,
},
description: {
color: changeOpacity(theme.centerChannelColor, 0.6),
...typography('Body', 75, 'Regular'),
marginTop: 3,
},
arrow: {
color: changeOpacity(theme.centerChannelColor, 0.25),
fontSize: 24,
},
labelContainer: {
flex: 0,
flexDirection: 'row',
},
};
});
type Props = {
action: (value: string | boolean) => void;
actionType: string;
actionValue?: string;
containerStyle?: StyleProp<ViewStyle>;
description?: string | ReactElement;
descriptionStyle?: StyleProp<TextStyle>;
icon?: string;
label: string | ReactElement;
labelStyle?: StyleProp<TextStyle>;
selected?: boolean;
testID?: string;
}
const BlockItem = ({
action,
actionType,
actionValue,
containerStyle,
description,
descriptionStyle,
icon,
label,
labelStyle,
selected,
testID = 'sectionItem',
}: Props) => {
const theme = useTheme();
const style = getStyleSheet(theme);
let actionComponent;
if (actionType === ActionTypes.SELECT && selected) {
const selectStyle = [style.arrow, {color: theme.linkColor}];
actionComponent = (
<CompassIcon
name='check'
style={selectStyle}
testID={`${testID}.selected`}
/>
);
} else if (actionType === ActionTypes.TOGGLE) {
actionComponent = (
<Switch
onValueChange={action}
value={selected}
testID={`${testID}.toggled.${selected}`}
/>
);
} else if (actionType === ActionTypes.ARROW) {
actionComponent = (
<CompassIcon
name='chevron-right'
style={style.arrow}
/>
);
}
const onPress = useCallback(() => {
action(actionValue || '');
}, [actionValue, action]);
const labelStyles = useMemo(() => {
if (icon) {
return [style.label, {marginLeft: 4}];
}
return [style.label, labelStyle];
}, [Boolean(icon), style]);
const component = (
<View
testID={testID}
style={[style.container, containerStyle]}
>
<View style={description ? style.doubleContainer : style.singleContainer}>
<View style={style.labelContainer}>
{Boolean(icon) && (
<CompassIcon
name={icon!}
size={24}
color={changeOpacity(theme.centerChannelColor, 0.6)}
/>
)}
<Text
style={labelStyles}
testID={`${testID}.label`}
>
{label}
</Text>
</View>
<Text
style={[style.description, descriptionStyle]}
testID={`${testID}.description`}
>
{description}
</Text>
</View>
{actionComponent}
</View>
);
if (actionType === ActionTypes.DEFAULT || actionType === ActionTypes.SELECT || actionType === ActionTypes.ARROW) {
return (
<TouchableOpacity onPress={onPress}>
{component}
</TouchableOpacity>
);
}
return component;
};
export default BlockItem;

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Switch, Text, TouchableOpacity, View} from 'react-native';
import {StyleProp, Switch, Text, TouchableOpacity, View, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
@@ -20,6 +20,7 @@ type Props = {
testID?: string;
type: OptionType;
value?: string;
containerStyle?: StyleProp<ViewStyle>;
}
const OptionType = {
@@ -79,7 +80,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const OptionItem = ({
action, description, destructive, icon,
info, label, selected,
testID = 'optionItem', type, value,
testID = 'optionItem', type, value, containerStyle,
}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
@@ -119,7 +120,7 @@ const OptionItem = ({
const component = (
<View
testID={testID}
style={styles.container}
style={[styles.container, containerStyle]}
>
<View style={styles.row}>
<View style={styles.labelContainer}>

View File

@@ -43,6 +43,7 @@ export const SELECT_TEAM = 'SelectTeam';
export const SERVER = 'Server';
export const SETTINGS = 'Settings';
export const SETTINGS_DISPLAY = 'SettingsDisplay';
export const SETTINGS_DISPLAY_THEME = 'SettingsDisplayTheme';
export const SETTINGS_NOTIFICATION = 'SettingsNotification';
export const SETTINGS_NOTIFICATION_AUTO_RESPONDER = 'SettingsNotificationAutoResponder';
export const SETTINGS_NOTIFICATION_MENTION = 'SettingsNotificationMention';
@@ -97,6 +98,7 @@ export default {
SERVER,
SETTINGS,
SETTINGS_DISPLAY,
SETTINGS_DISPLAY_THEME,
SETTINGS_NOTIFICATION,
SETTINGS_NOTIFICATION_AUTO_RESPONDER,
SETTINGS_NOTIFICATION_MENTION,

View File

@@ -397,20 +397,19 @@ export const observeOnlyUnreads = (database: Database) => {
);
};
export const observeAllowedThemes = (database: Database) => {
export const observeAllowedThemesKeys = (database: Database) => {
const defaultThemeKeys = Object.keys(Preferences.THEMES);
return observeConfigValue(database, 'AllowedThemes').pipe(
switchMap((allowedThemes) => {
let acceptableThemes = defaultThemeKeys;
if (allowedThemes) {
const allowedThemeKeys = (allowedThemes || '').split(',').filter(String);
const allowedThemeKeys = (allowedThemes ?? '').split(',').filter(String);
if (allowedThemeKeys.length) {
acceptableThemes = defaultThemeKeys.filter((k) => allowedThemeKeys.includes(k));
}
}
return acceptableThemes;
return of$(acceptableThemes);
}),
);
};

View File

@@ -17,11 +17,11 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import {SafeAreaView} from 'react-native-safe-area-context';
import Autocomplete from '@components/autocomplete';
import BlockItem from '@components/block_item';
import ErrorText from '@components/error_text';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import OptionItem from '@components/option_item';
import {General, Channel} from '@constants';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@@ -244,12 +244,12 @@ export default function ChannelInfoForm({
>
<View>
{showSelector && (
<BlockItem
<OptionItem
testID='channel_info_form.make_private'
label={makePrivateLabel}
description={makePrivateDescription}
action={handlePress}
actionType={'toggle'}
type={'toggle'}
selected={isPrivate}
icon={'lock-outline'}
/>

View File

@@ -61,14 +61,10 @@ Navigation.setLazyComponentRegistrator((screenName) => {
screen = withServerDatabase(require('@screens/apps_form').default);
break;
case Screens.BOTTOM_SHEET:
screen = withServerDatabase(
require('@screens/bottom_sheet').default,
);
screen = withServerDatabase(require('@screens/bottom_sheet').default);
break;
case Screens.BROWSE_CHANNELS:
screen = withServerDatabase(
require('@screens/browse_channels').default,
);
screen = withServerDatabase(require('@screens/browse_channels').default);
break;
case Screens.CHANNEL:
screen = withServerDatabase(require('@screens/channel').default);
@@ -83,14 +79,10 @@ Navigation.setLazyComponentRegistrator((screenName) => {
screen = withServerDatabase(require('@screens/create_or_edit_channel').default);
break;
case Screens.CUSTOM_STATUS:
screen = withServerDatabase(
require('@screens/custom_status').default,
);
screen = withServerDatabase(require('@screens/custom_status').default);
break;
case Screens.CUSTOM_STATUS_CLEAR_AFTER:
screen = withServerDatabase(
require('@screens/custom_status_clear_after').default,
);
screen = withServerDatabase(require('@screens/custom_status_clear_after').default);
break;
case Screens.CREATE_DIRECT_MESSAGE:
screen = withServerDatabase(require('@screens/create_direct_message').default);
@@ -99,9 +91,7 @@ Navigation.setLazyComponentRegistrator((screenName) => {
screen = withServerDatabase(require('@screens/edit_post').default);
break;
case Screens.EDIT_PROFILE:
screen = withServerDatabase(
require('@screens/edit_profile').default,
);
screen = withServerDatabase(require('@screens/edit_profile').default);
break;
case Screens.EDIT_SERVER:
screen = withIntl(require('@screens/edit_server').default);
@@ -125,8 +115,7 @@ Navigation.setLazyComponentRegistrator((screenName) => {
screen = withServerDatabase(require('@screens/interactive_dialog').default);
break;
case Screens.IN_APP_NOTIFICATION: {
const notificationScreen =
require('@screens/in_app_notification').default;
const notificationScreen = require('@screens/in_app_notification').default;
Navigation.registerComponent(Screens.IN_APP_NOTIFICATION, () =>
Platform.select({
default: notificationScreen,
@@ -154,9 +143,7 @@ Navigation.setLazyComponentRegistrator((screenName) => {
screen = withServerDatabase(require('@screens/pinned_messages').default);
break;
case Screens.POST_OPTIONS:
screen = withServerDatabase(
require('@screens/post_options').default,
);
screen = withServerDatabase(require('@screens/post_options').default);
break;
case Screens.REACTIONS:
screen = withServerDatabase(require('@screens/reactions').default);
@@ -164,6 +151,12 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.SETTINGS:
screen = withServerDatabase(require('@screens/settings').default);
break;
case Screens.SETTINGS_DISPLAY:
screen = withServerDatabase(require('@screens/settings/display').default);
break;
case Screens.SETTINGS_DISPLAY_THEME:
screen = withServerDatabase(require('@screens/settings/display_theme').default);
break;
case Screens.SETTINGS_NOTIFICATION:
screen = withServerDatabase(require('@screens/settings/notifications').default);
break;

View File

@@ -2,11 +2,15 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {Alert, Platform, ScrollView, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {goToScreen} from '@screens/navigation';
import SettingOption from '@screens/settings/setting_option';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
@@ -39,13 +43,20 @@ type DisplayProps = {
const Display = ({isTimezoneEnabled, isThemeSwitchingEnabled}: DisplayProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const intl = useIntl();
const onPressHandler = () => {
return Alert.alert(
'The functionality you are trying to use has not yet been implemented.',
);
};
const goToThemeSettings = preventDoubleTap(() => {
const screen = Screens.SETTINGS_DISPLAY_THEME;
const title = intl.formatMessage({id: 'display_settings.theme', defaultMessage: 'Theme'});
goToScreen(screen, title);
});
return (
<SafeAreaView
edges={['left', 'right']}
@@ -60,7 +71,7 @@ const Display = ({isTimezoneEnabled, isThemeSwitchingEnabled}: DisplayProps) =>
{isThemeSwitchingEnabled && (
<SettingOption
optionName='theme'
onPress={onPressHandler}
onPress={goToThemeSettings}
/>
)}
<SettingOption

View File

@@ -6,7 +6,7 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeAllowedThemes, observeConfigBooleanValue} from '@queries/servers/system';
import {observeAllowedThemesKeys, observeConfigBooleanValue} from '@queries/servers/system';
import {WithDatabaseArgs} from '@typings/database/database';
import DisplaySettings from './display';
@@ -15,9 +15,9 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const isTimezoneEnabled = observeConfigBooleanValue(database, 'ExperimentalTimezone');
const allowsThemeSwitching = observeConfigBooleanValue(database, 'EnableThemeSelection');
const allowedThemes = observeAllowedThemes(database);
const allowedThemeKeys = observeAllowedThemesKeys(database);
const isThemeSwitchingEnabled = combineLatest([allowsThemeSwitching, allowedThemes]).pipe(
const isThemeSwitchingEnabled = combineLatest([allowsThemeSwitching, allowedThemeKeys]).pipe(
switchMap(([ts, ath]) => {
return of$(ts && ath.length > 1);
}),

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import Block from '@components/block';
import OptionItem from '@components/option_item';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
label: {
color: theme.centerChannelColor,
...typography('Body', 200),
},
containerStyles: {
paddingHorizontal: 16,
},
};
});
type CustomThemeProps = {
customTheme: Theme;
setTheme: (themeKey: string) => void;
}
const CustomTheme = ({customTheme, setTheme}: CustomThemeProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const intl = useIntl();
return (
<Block
containerStyles={styles.containerStyles}
disableHeader={true}
>
<OptionItem
action={setTheme}
type='select'
value={customTheme.type}
label={intl.formatMessage({id: 'user.settings.display.custom_theme', defaultMessage: 'Custom Theme'})}
selected={theme.type?.toLowerCase() === customTheme.type?.toLowerCase()}
/>
</Block>
);
};
export default CustomTheme;

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import {ScrollView, View} from 'react-native';
import {savePreference} from '@actions/remote/preference';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import CustomTheme from '@screens/settings/display_theme/custom_theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {ThemeTiles} from './theme_tiles';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
},
wrapper: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
flex: 1,
paddingTop: 35,
},
};
});
type DisplayThemeProps = {
allowedThemeKeys: string[];
currentTeamId: string;
currentUserId: string;
}
const DisplayTheme = ({allowedThemeKeys, currentTeamId, currentUserId}: DisplayThemeProps) => {
const serverUrl = useServerUrl();
const theme = useTheme();
const [customTheme, setCustomTheme] = useState<Theme|null>();
const styles = getStyleSheet(theme);
useEffect(() => {
if (theme.type === 'custom') {
setCustomTheme(theme);
}
}, []);
const updateTheme = useCallback((selectedThemeKey: string) => {
const selectedTheme = allowedThemeKeys.find((tk) => tk === selectedThemeKey);
if (!selectedTheme) {
return;
}
const pref: PreferenceType = {
category: Preferences.CATEGORY_THEME,
name: currentTeamId,
user_id: currentUserId,
value: JSON.stringify(Preferences.THEMES[selectedTheme]),
};
savePreference(serverUrl, [pref]);
}, [serverUrl, allowedThemeKeys, currentTeamId]);
return (
<ScrollView style={styles.container}>
<View style={styles.wrapper}>
<ThemeTiles
allowedThemeKeys={allowedThemeKeys}
onThemeChange={updateTheme}
/>
{customTheme && (
<CustomTheme
customTheme={customTheme}
setTheme={updateTheme}
/>
)}
</View>
</ScrollView>
);
};
export default DisplayTheme;

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {
observeAllowedThemesKeys,
observeCurrentTeamId,
observeCurrentUserId,
} from '@queries/servers/system';
import {WithDatabaseArgs} from '@typings/database/database';
import DisplayTheme from './display_theme';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const currentTeamId = observeCurrentTeamId(database);
const currentUserId = observeCurrentUserId(database);
return {
allowedThemeKeys: observeAllowedThemesKeys(database),
currentTeamId,
currentUserId,
};
});
export default withDatabase(enhanced(DisplayTheme));

View File

@@ -0,0 +1,275 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Svg, {Rect, G, Circle} from 'react-native-svg';
import {changeOpacity} from '@utils/theme';
type ThemeThumbnailProps = {
borderColorBase: string;
borderColorMix: string;
theme: Theme;
width: number;
}
const ThemeThumbnail = ({borderColorBase, borderColorMix, theme, width}: ThemeThumbnailProps): JSX.Element => {
// the original height of the thumbnail
const baseWidth = 180;
const baseHeight = 134;
// calculate actual height proportionally to base size
const height = Math.round((width * baseHeight) / baseWidth);
// convenience values of various sub elements of the thumbnail
const sidebarWidth = 80;
const postsContainerWidth = 100;
const spacing = 8;
const rowHeight = 6;
const rowRadius = rowHeight / 2;
const postInputHeight = 10;
const postWidth = postsContainerWidth - (spacing * 2);
const channelNameWidth = sidebarWidth - (spacing * 3) - (rowHeight * 2);
const buttonWidth = postsContainerWidth - (spacing * 8);
return (
<Svg
width={width}
height={height}
viewBox={`-2 -2 ${baseWidth + 4} ${baseHeight + 4}`}
fill='none'
>
<Rect
fill={theme.centerChannelBg}
x='0'
y='0'
width={baseWidth}
height={baseHeight}
/>
<Rect
fill={theme.newMessageSeparator}
x={sidebarWidth}
y={(spacing * 4) + (rowHeight * 3)}
width={postsContainerWidth}
height='1'
/>
<Rect
fill={theme.buttonBg}
x={sidebarWidth + (spacing * 4)}
y={(spacing * 8) + (rowHeight * 6) + 1}
width={buttonWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
fill={changeOpacity(theme.centerChannelColor, 0.16)}
x={sidebarWidth + spacing}
y={(spacing * 9) + (rowHeight * 7) + 1}
width={postWidth}
height={postInputHeight}
rx={postInputHeight / 2}
/>
<Rect
fill={theme.centerChannelBg}
x={sidebarWidth + spacing + 1}
y={(spacing * 9) + (rowHeight * 7) + 2}
width={postWidth - 2}
height={postInputHeight - 2}
rx={(postInputHeight - 2) / 2}
/>
<G fill={changeOpacity(theme.centerChannelColor, 0.16)}>
<Rect
x={sidebarWidth + spacing}
y={spacing}
width={postWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={sidebarWidth + spacing}
y={(spacing * 2) + rowHeight}
width={postWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={sidebarWidth + spacing}
y={(spacing * 3) + (rowHeight * 2)}
width={postWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={sidebarWidth + spacing}
y={(spacing * 5) + (rowHeight * 3) + 1}
width={postWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={sidebarWidth + spacing}
y={(spacing * 6) + (rowHeight * 4) + 1}
width={postWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={sidebarWidth + spacing}
y={(spacing * 7) + (rowHeight * 5) + 1}
width={postWidth}
height={rowHeight}
rx={rowRadius}
/>
</G>
<G>
<Rect
fill={theme.sidebarBg}
x='0'
y='0'
width={sidebarWidth}
height={baseHeight}
/>
<G fill={changeOpacity(theme.sidebarText, 0.48)}>
<Circle
cx={spacing + rowRadius}
cy={spacing + rowRadius}
r={rowRadius}
/>
<Circle
cx={spacing + rowRadius}
cy={(spacing * 2) + rowHeight + rowRadius}
r={rowRadius}
/>
<Circle
cx={spacing + rowRadius}
cy={(spacing * 4) + (rowHeight * 3) + rowRadius}
r={rowRadius}
/>
<Circle
cx={spacing + rowRadius}
cy={(spacing * 5) + (rowHeight * 4) + rowRadius}
r={rowRadius}
/>
<Circle
cx={spacing + rowRadius}
cy={(spacing * 7) + (rowHeight * 6) + rowRadius}
r={rowRadius}
/>
<Circle
cx={spacing + rowRadius}
cy={(spacing * 8) + (rowHeight * 7) + rowRadius}
r={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={spacing}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 2) + rowHeight}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 4) + (rowHeight * 3)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 5) + (rowHeight * 4)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 6) + (rowHeight * 5)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 7) + (rowHeight * 6)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 8) + (rowHeight * 7)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 9) + (rowHeight * 8)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
</G>
<Circle
fill={theme.onlineIndicator}
cx={spacing + rowRadius}
cy={(spacing * 3) + (rowHeight * 2) + rowRadius}
r={rowRadius}
/>
<Circle
fill={theme.awayIndicator}
cx={spacing + rowRadius}
cy={(spacing * 6) + (rowHeight * 5) + rowRadius}
r={rowRadius}
/>
<Circle
fill={theme.dndIndicator}
cx={spacing + rowRadius}
cy={(spacing * 9) + (rowHeight * 8) + rowRadius}
r={rowRadius}
/>
<G fill={theme.sidebarUnreadText}>
<Circle
cx={(spacing * 2.5) + rowHeight + channelNameWidth}
cy={(spacing * 3) + (rowHeight * 2) + rowRadius}
r={rowRadius}
/>
<Rect
x={(spacing * 1.5) + rowHeight}
y={(spacing * 3) + (rowHeight * 2)}
width={channelNameWidth}
height={rowHeight}
rx={rowRadius}
/>
</G>
</G>
<Rect
x='-1'
y='-1'
width={baseWidth + 2}
height={baseHeight + 2}
rx='4'
stroke={borderColorBase}
strokeWidth='2'
/>
<Rect
x='-1'
y='-1'
width={baseWidth + 2}
height={baseHeight + 2}
rx='4'
stroke={borderColorMix}
strokeWidth='2'
/>
</Svg>
);
};
export default ThemeThumbnail;

View File

@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {Text, TouchableOpacity, useWindowDimensions, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {Preferences} from '@constants';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import ThemeThumbnail from './theme_thumbnail';
const TILE_PADDING = 8;
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flexDirection: 'column',
padding: TILE_PADDING,
marginTop: 8,
},
imageWrapper: {
position: 'relative',
alignItems: 'flex-start',
marginBottom: 12,
},
thumbnail: {
resizeMode: 'stretch',
},
check: {
position: 'absolute',
right: 5,
bottom: 5,
color: theme.sidebarTextActiveBorder,
},
label: {
color: theme.centerChannelColor,
...typography('Body', 200),
},
tilesContainer: {
marginBottom: 30,
paddingLeft: 8,
flexDirection: 'row',
flexWrap: 'wrap',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
},
};
});
type ThemeTileProps = {
action: (v: string) => void;
actionValue: string;
activeTheme: Theme;
label: React.ReactElement;
selected: boolean;
theme: Theme;
};
export const ThemeTile = ({
action,
actionValue,
activeTheme,
label,
selected,
theme,
}: ThemeTileProps) => {
const isTablet = useIsTablet();
const style = getStyleSheet(activeTheme);
const {width: deviceWidth} = useWindowDimensions();
const layoutStyle = useMemo(() => {
const tilesPerLine = isTablet ? 4 : 2;
const fullWidth = isTablet ? deviceWidth - 40 : deviceWidth;
return {
container: {
width: (fullWidth / tilesPerLine) - TILE_PADDING,
},
thumbnail: {
width: (fullWidth / tilesPerLine) - (TILE_PADDING + 16),
},
};
}, [isTablet, deviceWidth]);
const onPressHandler = useCallback(() => {
action(actionValue);
}, [action, actionValue]);
return (
<TouchableOpacity
onPress={onPressHandler}
style={[style.container, layoutStyle.container]}
>
<View style={[style.imageWrapper, layoutStyle.thumbnail]}>
<ThemeThumbnail
borderColorBase={selected ? activeTheme.sidebarTextActiveBorder : activeTheme.centerChannelBg}
borderColorMix={selected ? activeTheme.sidebarTextActiveBorder : changeOpacity(activeTheme.centerChannelColor, 0.16)}
theme={theme}
width={layoutStyle.thumbnail.width}
/>
{selected && (
<CompassIcon
name='check-circle'
size={31.2}
style={style.check}
/>
)}
</View>
{label}
</TouchableOpacity>
);
};
type ThemeTilesProps = {
allowedThemeKeys: string[];
onThemeChange: (v: string) => void;
}
export const ThemeTiles = ({allowedThemeKeys, onThemeChange}: ThemeTilesProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<View style={styles.tilesContainer}>
{
allowedThemeKeys.map((themeKey: string) => (
<ThemeTile
key={themeKey}
label={(
<Text style={styles.label}>
{themeKey}
</Text>
)}
action={onThemeChange}
actionValue={themeKey}
selected={theme.type?.toLowerCase() === themeKey.toLowerCase()}
theme={Preferences.THEMES[themeKey]}
activeTheme={theme}
/>
))
}
</View>
);
};

View File

@@ -6,9 +6,9 @@ import {useIntl} from 'react-intl';
import {View} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import BlockItem from '@components/block_item';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import OptionItem from '@components/option_item';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
@@ -108,15 +108,10 @@ const NotificationAutoResponder = ({currentUser}: NotificationAutoResponderProps
<View
style={styles.enabled}
>
<BlockItem
label={
<FormattedText
id='notification_settings.auto_responder.enabled'
defaultMessage='Enabled'
style={styles.label}
/>}
<OptionItem
label={intl.formatMessage({id: 'notification_settings.auto_responder.enabled', defaultMessage: 'Enabled'})}
action={onAutoResponseToggle}
actionType='toggle'
type='toggle'
selected={notifyProps.auto_responder_active === 'true'}
/>
</View>

View File

@@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
import {Alert, View} from 'react-native';
import Block from '@components/block';
import BlockItem from '@components/block_item';
import OptionItem from '@components/option_item';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import UserModel from '@typings/database/models/servers/user';
@@ -113,52 +113,44 @@ const MentionSettings = ({currentUser, mentionKeys}: MentionSectionProps) => {
>
{ Boolean(currentUser?.firstName) && (
<>
<BlockItem
<OptionItem
action={toggleFirstNameMention}
actionType='toggle'
containerStyle={styles.container}
description={intl.formatMessage({id: 'notification_settings.mentions.sensitiveName', defaultMessage: 'Your case sensitive first name'})}
descriptionStyle={styles.desc}
label={currentUser!.firstName}
labelStyle={styles.label}
selected={firstName}
type='toggle'
/>
<View style={styles.separator}/>
</>
)
}
{Boolean(currentUser?.username) && (
<BlockItem
<OptionItem
action={toggleUsernameMention}
actionType='toggle'
containerStyle={styles.container}
description={intl.formatMessage({id: 'notification_settings.mentions.sensitiveUsername', defaultMessage: 'Your non-case sensitive username'})}
descriptionStyle={styles.desc}
label={currentUser!.username}
labelStyle={styles.label}
selected={usernameMention}
type='toggle'
/>
)}
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={toggleChannelMentions}
actionType='toggle'
containerStyle={styles.container}
description={intl.formatMessage({id: 'notification_settings.mentions.channelWide', defaultMessage: 'Channel-wide mentions'})}
descriptionStyle={styles.desc}
label='@channel, @all, @here'
labelStyle={styles.label}
selected={channel}
type='toggle'
/>
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={goToNotificationSettingsMentionKeywords}
actionType='arrow'
containerStyle={styles.container}
description={mentionKeys || intl.formatMessage({id: 'notification_settings.mentions.keywordsDescription', defaultMessage: 'Other words that trigger a mention'})}
descriptionStyle={styles.desc}
label={intl.formatMessage({id: 'notification_settings.mentions.keywords', defaultMessage: 'Keywords'})}
labelStyle={styles.label}
type='arrow'
/>
</Block>
);

View File

@@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
import {View} from 'react-native';
import Block from '@components/block';
import BlockItem from '@components/block_item';
import OptionItem from '@components/option_item';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -52,33 +52,30 @@ const ReplySettings = () => {
headerText={replyHeaderText}
headerStyles={styles.upperCase}
>
<BlockItem
<OptionItem
action={setReplyNotifications}
actionType='select'
actionValue='any'
type='select'
value='any'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.threads_start_participate', defaultMessage: 'Threads that I start or participate in'})}
labelStyle={styles.label}
selected={replyNotificationType === 'any'}
/>
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={setReplyNotifications}
actionType='select'
actionValue='root'
type='select'
value='root'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.threads_start', defaultMessage: 'Threads that I start'})}
labelStyle={styles.label}
selected={replyNotificationType === 'root'}
/>
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={setReplyNotifications}
actionType='select'
actionValue='never'
type='select'
value='never'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.threads_mentions', defaultMessage: 'Mentions in threads'})}
labelStyle={styles.label}
selected={replyNotificationType === 'never'}
/>
</Block>

View File

@@ -6,8 +6,8 @@ import {useIntl} from 'react-intl';
import {View} from 'react-native';
import Block from '@components/block';
import BlockItem from '@components/block_item';
import FormattedText from '@components/formatted_text';
import OptionItem from '@components/option_item';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -37,6 +37,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingHorizontal: 15,
paddingVertical: 10,
},
container: {
paddingHorizontal: 8,
},
};
});
@@ -57,34 +60,34 @@ const MobileSendPush = ({sendPushNotifications, pushStatus, setMobilePushPref}:
>
{sendPushNotifications &&
<>
<BlockItem
<OptionItem
action={setMobilePushPref}
actionType='select'
actionValue='all'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.pushNotification.allActivity', defaultMessage: 'For all activity'})}
labelStyle={styles.label}
selected={pushStatus === 'all'}
testID='notification_settings.pushNotification.allActivity'
type='select'
value='all'
/>
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={setMobilePushPref}
actionType='select'
actionValue='mention'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.pushNotification.onlyMentions', defaultMessage: 'Only for mentions and direct messages'})}
labelStyle={styles.label}
selected={pushStatus === 'mention'}
testID='notification_settings.pushNotification.onlyMentions'
type='select'
value='mention'
/>
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={setMobilePushPref}
actionType='select'
actionValue='none'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.pushNotification.never', defaultMessage: 'Never'})}
labelStyle={styles.label}
selected={pushStatus === 'none'}
testID='notification_settings.pushNotification.never'
type='select'
value='none'
/>
</>
}

View File

@@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
import {View} from 'react-native';
import Block from '@components/block';
import BlockItem from '@components/block_item';
import OptionItem from '@components/option_item';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -32,6 +32,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
...typography('Body', 100, 'Regular'),
},
container: {
paddingHorizontal: 8,
},
};
});
@@ -49,31 +52,31 @@ const MobilePushStatus = ({pushStatus, setMobilePushStatus}: MobilePushStatusPro
headerText={headerText}
headerStyles={styles.upperCase}
>
<BlockItem
<OptionItem
action={setMobilePushStatus}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.mobile.online', defaultMessage: 'Online, away or offline'})}
labelStyle={styles.label}
action={setMobilePushStatus}
actionType='select'
actionValue='online'
selected={pushStatus === 'online'}
type='select'
value='online'
/>
<View style={styles.separator}/>
<BlockItem
<OptionItem
action={setMobilePushStatus}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.mobile.away', defaultMessage: 'Away or offline'})}
labelStyle={styles.label}
action={setMobilePushStatus}
actionType='select'
actionValue='away'
selected={pushStatus === 'away'}
type='select'
value='away'
/>
<View style={styles.separator}/>
<BlockItem
label={intl.formatMessage({id: 'notification_settings.mobile.offline', defaultMessage: 'Offline'})}
labelStyle={styles.label}
<OptionItem
action={setMobilePushStatus}
actionType='select'
actionValue='offline'
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.mobile.offline', defaultMessage: 'Offline'})}
selected={pushStatus === 'offline'}
type='select'
value='offline'
/>
</Block>
);

View File

@@ -2,11 +2,11 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {View} from 'react-native';
import Block from '@components/block';
import BlockItem from '@components/block_item';
import FormattedText from '@components/formatted_text';
import OptionItem from '@components/option_item';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -49,6 +49,7 @@ type MobilePushThreadProps = {
const MobilePushThread = ({pushThread, onMobilePushThreadChanged}: MobilePushThreadProps) => {
const theme = useTheme();
const intl = useIntl();
const styles = getStyleSheet(theme);
return (
@@ -58,17 +59,11 @@ const MobilePushThread = ({pushThread, onMobilePushThreadChanged}: MobilePushThr
headerStyles={styles.upperCase}
containerStyles={styles.area}
>
<BlockItem
label={(
<FormattedText
id='notification_settings.push_threads.description'
defaultMessage={'Notify me about all replies to threads I\'m following'}
style={styles.label}
/>
)}
<OptionItem
action={onMobilePushThreadChanged}
actionType='toggle'
label={intl.formatMessage({id: 'notification_settings.push_threads.description', defaultMessage: 'Notify me about all replies to threads I\'m following'})}
selected={pushThread === 'all'}
type='toggle'
/>
<View style={styles.separator}/>
</Block>

View File

@@ -227,6 +227,7 @@
"custom_status.suggestions.working_from_home": "Working from home",
"date_separator.today": "Today",
"date_separator.yesterday": "Yesterday",
"display_settings.theme": "Theme",
"download.error": "Unable to download the file. Try again later",
"edit_post.editPost": "Edit the post...",
"edit_post.save": "Save",
@@ -711,6 +712,7 @@
"unreads.empty.title": "No more unreads",
"user.edit_profile.email.auth_service": "Login occurs through {service}. Email cannot be updated. Email address used for notifications is {email}.",
"user.edit_profile.email.web_client": "Email must be updated using a web client or desktop application.",
"user.settings.display.custom_theme": "Custom Theme",
"user.settings.general.email": "Email",
"user.settings.general.field_handled_externally": "Some fields below are handled through your login provider. If you want to change them, youll need to do so through your login provider.",
"user.settings.general.firstName": "First Name",