MM-45221 - Gekidou Settings fixes - part 2 (#6491)

* modifying setting option

* navigates to email screen

* UI construction [in progress]

* hooking up withObservables

* email settings - need to save now

* adding a bit of paddings

* setting initial value

* Update notification_email.tsx

* UI Polish - main setting screen

* UI Polish - Mention

* UI Polish - Notification main screen

* code clean up

* code clean up

* UI Polish Notification

* UI Polish

* code clean up

* UI Polish - OOO

* fix observable for email interval

* fix ooo

* fix ooo

* added setting_row_label component

* further clean up

* UI Polish - Display - [ IN PROGRESS ]

* UI Polish - Display - [ IN PROGRESS ]

* UI Polish - Timezone Select [ IN PROGRESS ]

* Update index.test.tsx.snap

* Update app/screens/settings/notification_email/notification_email.tsx

Co-authored-by: Daniel Espino García <larkox@gmail.com>

* refactor after review

* update option_item so that action can accept type React.Dispatch<React.SetStateAction

* UI - Polish - Display Theme

UI Polish - Display/Theme [ IN PROGRESS ]

UI - Polish - Display Save button

* UI - Polish - Display Clock

UI - Polish - Display Clock

* UI Polish - Display - Timezone

UI Polish - Display Timezone

added the save button

UI Polish - Display/TZ

UI Polish - Display Timezone

minor refactoring

code clean up

code clean up

* UI Polish - Adv Settings

* UI Polish - Settings/About

UI Polish - Settings/About

* UI Polish - Radio Button

UI Polish - Radio Button

* UI Polish - Android Polishing [ IN PROGRESS ]

* UI Polish - Timezone

fix timezone

fix timezone select

fix timezone select ui

Update index.tsx

* UI Polish - Display/Theme

UI Polish - Custom Theme - Checked Radio btn

* Update en.json

* tweaks to settings styles

* further tweaks to spacing

* Revert "Merge branch 'gekidou-settings-finale' of https://github.com/mattermost/mattermost-mobile into gekidou-settings-finale"

This reverts commit f1fd26987e, reversing
changes made to 684ba6a19c.

* added space before 'default'

* Update index.test.tsx.snap

* Revert "Revert "Merge branch 'gekidou-settings-finale' of https://github.com/mattermost/mattermost-mobile into gekidou-settings-finale""

This reverts commit ce77127cb2.

Co-authored-by: Daniel Espino García <larkox@gmail.com>
Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
This commit is contained in:
Avinash Lingaloo
2022-07-27 22:35:57 +04:00
committed by GitHub
parent 3a3123674a
commit adde833ea9
34 changed files with 769 additions and 597 deletions

View File

@@ -112,7 +112,7 @@ exports[`DrawerItem should match snapshot 1`] = `
style={
Array [
Object {
"backgroundColor": "rgba(63,67,80,0.2)",
"backgroundColor": "rgba(63,67,80,0.12)",
"height": 1,
},
undefined,

View File

@@ -44,7 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
includeFontPadding: false,
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.12),
height: 1,
},
chevron: {

View File

@@ -9,29 +9,15 @@ import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
export type OptionItemProps = {
action?: (React.Dispatch<React.SetStateAction<string | boolean>>)|((value: string | boolean) => void) ;
description?: string;
inline?: boolean;
destructive?: boolean;
icon?: string;
info?: string;
label: string;
selected?: boolean;
testID?: string;
type: OptionType;
value?: string;
containerStyle?: StyleProp<ViewStyle>;
optionLabelTextStyle?: StyleProp<TextStyle>;
optionDescriptionTextStyle?: StyleProp<TextStyle>;
}
import RadioItem, {RadioItemProps} from './radio_item';
const OptionType = {
ARROW: 'arrow',
DEFAULT: 'default',
TOGGLE: 'toggle',
SELECT: 'select',
NONE: 'none',
RADIO: 'radio',
SELECT: 'select',
TOGGLE: 'toggle',
} as const;
type OptionType = typeof OptionType[keyof typeof OptionType];
@@ -96,6 +82,23 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
export type OptionItemProps = {
action?: (React.Dispatch<React.SetStateAction<string | boolean>>)|((value: string | boolean) => void);
containerStyle?: StyleProp<ViewStyle>;
description?: string;
destructive?: boolean;
icon?: string;
info?: string;
inline?: boolean;
label: string;
optionDescriptionTextStyle?: StyleProp<TextStyle>;
optionLabelTextStyle?: StyleProp<TextStyle>;
radioItemProps?: Partial<RadioItemProps>;
selected?: boolean;
testID?: string;
type: OptionType;
value?: string;
}
const OptionItem = ({
action,
containerStyle,
@@ -107,6 +110,7 @@ const OptionItem = ({
label,
optionDescriptionTextStyle,
optionLabelTextStyle,
radioItemProps,
selected,
testID = 'optionItem',
type,
@@ -136,6 +140,7 @@ const OptionItem = ({
}, [destructive, styles, isInLine]);
let actionComponent;
let radioComponent;
if (type === OptionType.SELECT && selected) {
actionComponent = (
<CompassIcon
@@ -145,6 +150,13 @@ const OptionItem = ({
testID={`${testID}.selected`}
/>
);
} else if (type === OptionType.RADIO) {
radioComponent = (
<RadioItem
selected={Boolean(selected)}
{...radioItemProps}
/>
);
} else if (type === OptionType.TOGGLE) {
const trackColor = Platform.select({
ios: {true: theme.buttonBg, false: changeOpacity(theme.centerChannelColor, 0.16)},
@@ -192,6 +204,7 @@ const OptionItem = ({
/>
</View>
)}
{type === OptionType.RADIO && radioComponent}
<View style={labelStyle}>
<Text
style={[optionLabelTextStyle, labelTextStyle]}
@@ -224,7 +237,7 @@ const OptionItem = ({
</View>
);
if (type === OptionType.DEFAULT || type === OptionType.SELECT || type === OptionType.ARROW) {
if (type === OptionType.DEFAULT || type === OptionType.SELECT || type === OptionType.ARROW || type === OptionType.RADIO) {
return (
<TouchableOpacity onPress={onPress}>
{component}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const RADIO_SIZE = 24;
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
ring: {
height: RADIO_SIZE,
width: RADIO_SIZE,
borderRadius: RADIO_SIZE / 2,
marginRight: 16,
borderWidth: 2,
borderColor: theme.buttonBg,
alignItems: 'center',
justifyContent: 'center',
},
inActive: {
borderColor: changeOpacity(theme.centerChannelColor, 0.56),
},
center: {
height: RADIO_SIZE / 2,
width: RADIO_SIZE / 2,
borderRadius: RADIO_SIZE / 2,
backgroundColor: theme.buttonBg,
},
checkedBodyContainer: {
backgroundColor: theme.buttonBg,
},
};
});
export type RadioItemProps = {
selected: boolean;
checkedBody?: boolean;
}
const RadioItem = ({selected, checkedBody}: RadioItemProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const getBody = useCallback(() => {
if (checkedBody) {
return (
<View style={styles.checkedBodyContainer}>
<CompassIcon
color={theme.buttonColor}
name='check'
size={RADIO_SIZE / 1.5}
/>
</View>
);
}
return (<View style={styles.center}/>);
}, [checkedBody]);
return (
<View style={[styles.ring, !selected && styles.inActive]}>
{selected && getBody()}
</View>
);
};
export default RadioItem;

View File

@@ -1,345 +0,0 @@
// 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 React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Alert, ScrollView, Text, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {SafeAreaView} from 'react-native-safe-area-context';
import Config from '@assets/config.json';
import AppVersion from '@components/app_version';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import AboutLinks from '@constants/about_links';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {observeConfig, observeLicense} from '@queries/servers/system';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import LearnMore from './learn_more';
import ServerVersion from './server_version';
import Subtitle from './subtitle';
import Title from './title';
import TosPrivacyContainer from './tos_privacy';
import type {WithDatabaseArgs} from '@typings/database/database';
const MATTERMOST_BUNDLE_IDS = ['com.mattermost.rnbeta', 'com.mattermost.rn'];
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
},
scrollViewContent: {
paddingBottom: 30,
},
logoContainer: {
alignItems: 'center',
flex: 1,
height: 200,
paddingVertical: 40,
},
infoContainer: {
flex: 1,
flexDirection: 'column',
paddingHorizontal: 20,
},
titleContainer: {
flex: 1,
marginBottom: 20,
},
title: {
fontSize: 22,
color: theme.centerChannelColor,
},
subtitle: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 19,
marginBottom: 15,
},
info: {
color: theme.centerChannelColor,
fontSize: 16,
lineHeight: 19,
},
licenseContainer: {
flex: 1,
flexDirection: 'row',
marginTop: 20,
},
noticeContainer: {
flex: 1,
flexDirection: 'column',
},
noticeLink: {
color: theme.linkColor,
fontSize: 11,
lineHeight: 13,
},
hashContainer: {
flex: 1,
flexDirection: 'column',
},
footerGroup: {
flex: 1,
},
footerTitleText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 11,
fontFamily: 'OpenSans-SemiBold',
lineHeight: 13,
},
footerText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 11,
lineHeight: 13,
marginBottom: 10,
},
copyrightText: {
marginBottom: 0,
},
tosPrivacyContainer: {
flex: 1,
flexDirection: 'row',
marginBottom: 10,
},
};
});
type ConnectedAboutProps = {
config: ClientConfig;
license: ClientLicense;
}
const ConnectedAbout = ({config, license}: ConnectedAboutProps) => {
const intl = useIntl();
const theme = useTheme();
const style = getStyleSheet(theme);
const openURL = useCallback((url: string) => {
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(url, onError);
}, []);
const handleAboutTeam = useCallback(preventDoubleTap(() => {
return openURL(Config.AboutTeamURL);
}), []);
const handleAboutEnterprise = useCallback(preventDoubleTap(() => {
return openURL(Config.AboutEnterpriseURL);
}), []);
const handlePlatformNotice = useCallback(preventDoubleTap(() => {
return openURL(Config.PlatformNoticeURL);
}), []);
const handleMobileNotice = useCallback(preventDoubleTap(() => {
return openURL(Config.MobileNoticeURL);
}), []);
const handleTermsOfService = useCallback(preventDoubleTap(() => {
return openURL(AboutLinks.TERMS_OF_SERVICE);
}), []);
const handlePrivacyPolicy = useCallback(preventDoubleTap(() => {
return openURL(AboutLinks.PRIVACY_POLICY);
}), []);
return (
<SafeAreaView
edges={['left', 'right']}
style={style.container}
testID='about.screen'
>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
testID='about.scroll_view'
>
<View style={style.logoContainer}>
<CompassIcon
name='mattermost'
color={theme.centerChannelColor}
size={120}
testID='about.logo'
/>
</View>
<View style={style.infoContainer}>
<View style={style.titleContainer}>
<Text
style={style.title}
testID='about.site_name'
>
{`${config.SiteName} `}
</Text>
<Title
config={config}
license={license}
/>
</View>
<Subtitle config={config}/>
<AppVersion
isWrapped={false}
textStyle={style.info}
/>
<ServerVersion config={config}/>
<FormattedText
id={t('mobile.about.database')}
defaultMessage='Database: {type}'
style={style.info}
values={{
type: config.SQLDriverName,
}}
testID='about.database'
/>
{license.IsLicensed === 'true' && (
<View style={style.licenseContainer}>
<FormattedText
id={t('mobile.about.licensed')}
defaultMessage='Licensed to: {company}'
style={style.info}
values={{
company: license.Company,
}}
testID='about.licensee'
/>
</View>
)}
<LearnMore
config={config}
onHandleAboutEnterprise={handleAboutEnterprise}
onHandleAboutTeam={handleAboutTeam}
/>
{!MATTERMOST_BUNDLE_IDS.includes(DeviceInfo.getBundleId()) &&
<FormattedText
id={t('mobile.about.powered_by')}
defaultMessage='{site} is powered by Mattermost'
style={style.footerText}
values={{
site: config.SiteName,
}}
testID='about.powered_by'
/>
}
<FormattedText
id={t('mobile.about.copyright')}
defaultMessage='Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved'
style={[style.footerText, style.copyrightText]}
values={{
currentYear: new Date().getFullYear(),
}}
testID='about.copyright'
/>
<View style={style.tosPrivacyContainer}>
<TosPrivacyContainer
config={config}
onPressTOS={handleTermsOfService}
onPressPrivacyPolicy={handlePrivacyPolicy}
/>
</View>
<View style={style.noticeContainer}>
<View style={style.footerGroup}>
<FormattedText
id={t('mobile.notice_text')}
defaultMessage='Mattermost is made possible by the open source software used in our {platform} and {mobile}.'
style={style.footerText}
values={{
platform: (
<FormattedText
id={t('mobile.notice_platform_link')}
defaultMessage='server'
style={style.noticeLink}
onPress={handlePlatformNotice}
/>
),
mobile: (
<FormattedText
id={t('mobile.notice_mobile_link')}
defaultMessage='mobile apps'
style={[style.noticeLink, {marginLeft: 5}]}
onPress={handleMobileNotice}
/>
),
}}
testID='about.notice_text'
/>
</View>
</View>
<View style={style.hashContainer}>
<View style={style.footerGroup}>
<FormattedText
id={t('about.hash')}
defaultMessage='Build Hash:'
style={style.footerTitleText}
testID='about.build_hash.title'
/>
<Text
style={style.footerText}
testID='about.build_hash.value'
>
{config.BuildHash}
</Text>
</View>
<View style={style.footerGroup}>
<FormattedText
id={t('about.hashee')}
defaultMessage='EE Build Hash:'
style={style.footerTitleText}
testID='about.build_hash_enterprise.title'
/>
<Text
style={style.footerText}
testID='about.build_hash_enterprise.value'
>
{config.BuildHashEnterprise}
</Text>
</View>
</View>
<View style={style.footerGroup}>
<FormattedText
id={t('about.date')}
defaultMessage='Build Date:'
style={style.footerTitleText}
testID='about.build_date.title'
/>
<Text
style={style.footerText}
testID='about.build_date.value'
>
{config.BuildDate}
</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
config: observeConfig(database),
license: observeLicense(database),
}));
export default withDatabase(enhanced(ConnectedAbout));

View File

@@ -55,7 +55,7 @@ Navigation.setLazyComponentRegistrator((screenName) => {
let extraStyles: StyleProp<ViewStyle>;
switch (screenName) {
case Screens.ABOUT:
screen = withServerDatabase(require('@screens/about').default);
screen = withServerDatabase(require('@screens/settings/about').default);
break;
case Screens.APPS_FORM:
screen = withServerDatabase(require('@screens/apps_form').default);

View File

@@ -0,0 +1,325 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Alert, Text, View} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import Config from '@assets/config.json';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import AboutLinks from '@constants/about_links';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import SettingContainer from '@screens/settings/setting_container';
import SettingSeparator from '@screens/settings/settings_separator';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {tryOpenURL} from '@utils/url';
import LearnMore from './learn_more';
import Subtitle from './subtitle';
import Title from './title';
import TosPrivacyContainer from './tos_privacy';
const MATTERMOST_BUNDLE_IDS = ['com.mattermost.rnbeta', 'com.mattermost.rn'];
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
logoContainer: {
alignItems: 'center',
paddingHorizontal: 20,
marginTop: 20,
},
lineStyles: {
width: '100%',
marginTop: 40,
marginBottom: 24,
},
leftHeading: {
...typography('Body', 200, 'SemiBold'),
marginRight: 8,
color: theme.centerChannelColor,
},
rightHeading: {
...typography('Body', 200, 'Regular'),
color: theme.centerChannelColor,
},
infoContainer: {
flexDirection: 'column',
paddingHorizontal: 20,
},
info: {
color: theme.centerChannelColor,
...typography('Body', 200, 'Regular'),
},
licenseContainer: {
flexDirection: 'row',
marginTop: 20,
},
noticeContainer: {
flexDirection: 'column',
},
noticeLink: {
color: theme.linkColor,
...typography('Body', 50, 'Regular'),
},
hashContainer: {
flexDirection: 'column',
},
footerTitleText: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 50, 'SemiBold'),
},
footerText: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 50),
marginBottom: 10,
},
copyrightText: {
marginBottom: 0,
},
tosPrivacyContainer: {
flexDirection: 'row',
marginBottom: 10,
},
group: {
flexDirection: 'row',
},
};
});
type AboutProps = {
config: ClientConfig;
license: ClientLicense;
}
const About = ({config, license}: AboutProps) => {
const intl = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const openURL = useCallback((url: string) => {
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'settings.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'settings.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(url, onError);
}, []);
const handleAboutTeam = useCallback(preventDoubleTap(() => {
return openURL(Config.AboutTeamURL);
}), []);
const handleAboutEnterprise = useCallback(preventDoubleTap(() => {
return openURL(Config.AboutEnterpriseURL);
}), []);
const handlePlatformNotice = useCallback(preventDoubleTap(() => {
return openURL(Config.PlatformNoticeURL);
}), []);
const handleMobileNotice = useCallback(preventDoubleTap(() => {
return openURL(Config.MobileNoticeURL);
}), []);
const handleTermsOfService = useCallback(preventDoubleTap(() => {
return openURL(AboutLinks.TERMS_OF_SERVICE);
}), []);
const handlePrivacyPolicy = useCallback(preventDoubleTap(() => {
return openURL(AboutLinks.PRIVACY_POLICY);
}), []);
const serverVersion = useMemo(() => {
const buildNumber = config.BuildNumber;
const version = config.Version;
let id = t('settings.about.serverVersion');
let defaultMessage = '{version} (Build {number})';
let values: {version: string; number?: string} = {
version,
number: buildNumber,
};
if (buildNumber === version) {
id = t('settings.about.serverVersionNoBuild');
defaultMessage = '{version}';
values = {
version,
number: undefined,
};
}
return {
id, defaultMessage, values,
};
}, [config]);
return (
<SettingContainer>
<View style={styles.logoContainer}>
<CompassIcon
color={theme.centerChannelColor}
name='mattermost'
size={88}
testID='about.logo'
/>
<Title
config={config}
license={license}
/>
<Subtitle config={config}/>
<SettingSeparator lineStyles={styles.lineStyles}/>
</View>
<View style={styles.infoContainer}>
<View style={styles.group}>
<Text style={styles.leftHeading}>
{intl.formatMessage({id: 'settings.about.version', defaultMessage: 'App Version:'})}
</Text>
<Text style={styles.rightHeading}>
{intl.formatMessage({id: 'settings.about.build', defaultMessage: '{version} (Build {number})'},
{version: DeviceInfo.getVersion(), number: DeviceInfo.getBuildNumber()})}
</Text>
</View>
<View style={styles.group}>
<Text style={styles.leftHeading}>
{intl.formatMessage({id: 'settings.about.serverVersion', defaultMessage: 'Server Version:'})}
</Text>
<Text style={styles.rightHeading}>
{intl.formatMessage({id: serverVersion.id, defaultMessage: serverVersion.defaultMessage}, serverVersion.values)}
</Text>
</View>
<View style={styles.group}>
<Text style={styles.leftHeading}>
{intl.formatMessage({id: 'settings.about.database', defaultMessage: 'Database:'})}
</Text>
<Text style={styles.rightHeading}>
{intl.formatMessage({id: 'settings.about.database.value', defaultMessage: `${config.SQLDriverName}`})}
</Text>
</View>
{license.IsLicensed === 'true' && (
<View style={styles.licenseContainer}>
<FormattedText
defaultMessage='Licensed to: {company}'
id={t('settings.about.licensed')}
style={styles.info}
testID='about.licensee'
values={{company: license.Company}}
/>
</View>
)}
<LearnMore
config={config}
onHandleAboutEnterprise={handleAboutEnterprise}
onHandleAboutTeam={handleAboutTeam}
/>
{!MATTERMOST_BUNDLE_IDS.includes(DeviceInfo.getBundleId()) &&
<FormattedText
defaultMessage='{site} is powered by Mattermost'
id={t('settings.about.powered_by')}
style={styles.footerText}
testID='about.powered_by'
values={{site: config.SiteName}}
/>
}
<FormattedText
defaultMessage='Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved'
id={t('settings.about.copyright')}
style={[styles.footerText, styles.copyrightText]}
testID='about.copyright'
values={{currentYear: new Date().getFullYear()}}
/>
<View style={styles.tosPrivacyContainer}>
<TosPrivacyContainer
config={config}
onPressPrivacyPolicy={handlePrivacyPolicy}
onPressTOS={handleTermsOfService}
/>
</View>
<View style={styles.noticeContainer}>
<FormattedText
id={t('settings.notice_text')}
defaultMessage='Mattermost is made possible by the open source software used in our {platform} and {mobile}.'
style={styles.footerText}
values={{
platform: (
<FormattedText
defaultMessage='server'
id={t('settings.notice_platform_link')}
onPress={handlePlatformNotice}
style={styles.noticeLink}
/>
),
mobile: (
<FormattedText
defaultMessage='mobile apps'
id={t('settings.notice_mobile_link')}
onPress={handleMobileNotice}
style={[styles.noticeLink, {marginLeft: 5}]}
/>
),
}}
testID='about.notice_text'
/>
</View>
<View style={styles.hashContainer}>
<View style={styles.footerGroup}>
<FormattedText
defaultMessage='Build Hash:'
id={t('about.hash')}
style={styles.footerTitleText}
testID='about.build_hash.title'
/>
<Text
style={styles.footerText}
testID='about.build_hash.value'
>
{config.BuildHash}
</Text>
</View>
<View style={styles.footerGroup}>
<FormattedText
defaultMessage='EE Build Hash:'
id={t('about.hashee')}
style={styles.footerTitleText}
testID='about.build_hash_enterprise.title'
/>
<Text
style={styles.footerText}
testID='about.build_hash_enterprise.value'
>
{config.BuildHashEnterprise}
</Text>
</View>
</View>
<View style={[styles.footerGroup, {marginBottom: 20}]}>
<FormattedText
defaultMessage='Build Date:'
id={t('about.date')}
style={styles.footerTitleText}
testID='about.build_date.title'
/>
<Text
style={styles.footerText}
testID='about.build_date.value'
>
{config.BuildDate}
</Text>
</View>
</View>
</SettingContainer>
);
};
export default About;

View File

@@ -0,0 +1,18 @@
// 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 {observeConfig, observeLicense} from '@queries/servers/system';
import About from './about';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
config: observeConfig(database),
license: observeLicense(database),
}));
export default withDatabase(enhanced(About));

View File

@@ -9,6 +9,25 @@ import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
learnContainer: {
flex: 1,
flexDirection: 'column',
marginVertical: 20,
},
learn: {
color: theme.centerChannelColor,
...typography('Body', 200, 'Regular'),
},
learnLink: {
color: theme.linkColor,
...typography('Body', 200, 'Regular'),
},
};
});
type LearnMoreProps = {
config: ClientConfig;
@@ -52,22 +71,4 @@ const LearnMore = ({config, onHandleAboutEnterprise, onHandleAboutTeam}: LearnMo
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
learnContainer: {
flex: 1,
flexDirection: 'column',
marginVertical: 20,
},
learn: {
color: theme.centerChannelColor,
fontSize: 16,
},
learnLink: {
color: theme.linkColor,
fontSize: 16,
},
};
});
export default LearnMore;

View File

@@ -7,11 +7,22 @@ import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
subtitle: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Heading', 400, 'Regular'),
textAlign: 'center',
paddingHorizontal: 36,
},
};
});
type SubtitleProps = {
config: ClientConfig;
}
const Subtitle = ({config}: SubtitleProps) => {
const theme = useTheme();
const style = getStyleSheet(theme);
@@ -21,7 +32,7 @@ const Subtitle = ({config}: SubtitleProps) => {
if (config.BuildEnterpriseReady === 'true') {
id = t('about.enterpriseEditionSt');
defaultMessage = 'Modern communication from behind your firewall.';
defaultMessage = 'Modern communication from\n behind your firewall.';
}
return (
@@ -34,14 +45,4 @@ const Subtitle = ({config}: SubtitleProps) => {
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
subtitle: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 19,
marginBottom: 15,
},
};
});
export default Subtitle;

View File

@@ -2,17 +2,34 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Text} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
title: {
...typography('Heading', 800, 'SemiBold'),
color: theme.centerChannelColor,
paddingHorizontal: 36,
},
spacerTop: {
marginTop: 8,
},
spacerBottom: {
marginBottom: 8,
},
};
});
type TitleProps = {
config: ClientConfig;
license: ClientLicense;
}
const Title = ({config, license}: TitleProps) => {
const theme = useTheme();
const style = getStyleSheet(theme);
@@ -29,23 +46,24 @@ const Title = ({config, license}: TitleProps) => {
defaultMessage = 'Enterprise Edition';
}
}
return (
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.title}
testID='about.title'
/>
<>
<Text
style={[style.title, style.spacerTop]}
testID='about.site_name'
>
{`${config.SiteName} `}
</Text>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={[style.title, style.spacerBottom]}
testID='about.title'
/>
</>
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
title: {
fontSize: 22,
color: theme.centerChannelColor,
},
};
});
export default Title;

View File

@@ -8,6 +8,7 @@ import {TouchableOpacity} from 'react-native';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {popTopScreen} from '@screens/navigation';
import SettingSeparator from '@screens/settings/settings_separator';
import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -86,6 +87,7 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
label={intl.formatMessage({id: 'advanced_settings.delete_data', defaultMessage: 'Delete Documents & Data'})}
type='none'
/>
<SettingSeparator/>
</TouchableOpacity>
</SettingContainer>
);

View File

@@ -3,6 +3,7 @@
import React, {useMemo} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
@@ -39,6 +40,12 @@ const TIMEZONE_FORMAT = [
},
];
const styles = StyleSheet.create({
title: {
textTransform: 'capitalize',
},
});
type DisplayProps = {
currentUser: UserModel;
hasMilitaryTimeFormat: boolean;
@@ -76,9 +83,10 @@ const Display = ({currentUser, hasMilitaryTimeFormat, isThemeSwitchingEnabled, i
<SettingItem
optionName='theme'
onPress={goToThemeSettings}
rightComponent={
rightComponent={Boolean(theme.type) &&
<SettingRowLabel
text={theme.type || ''}
text={theme.type!}
textStyle={styles.title}
/>
}
/>

View File

@@ -10,7 +10,6 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import {popTopScreen, setButtons} from '@screens/navigation';
import {getSaveButton} from '../config';
@@ -19,11 +18,6 @@ import SettingContainer from '../setting_container';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
const footer = {
id: t('settings_display.clock.preferTime'),
defaultMessage: 'Select how you prefer time displayed.',
};
const CLOCK_TYPE = {
NORMAL: 'NORMAL',
MILITARY: 'MILITARY',
@@ -79,11 +73,11 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ
<SettingContainer>
<SettingBlock
disableHeader={true}
footerText={footer}
>
<SettingOption
action={onSelectClockPreference}
label={intl.formatMessage({id: 'settings_display.clock.normal', defaultMessage: '12-hour clock (example: 4:00 PM)'})}
label={intl.formatMessage({id: 'settings_display.clock.standard', defaultMessage: '12-hour clock'})}
description={intl.formatMessage({id: 'settings_display.clock.normal.desc', defaultMessage: 'Example: 4:00 PM'})}
selected={!isMilitaryTimeFormat}
testID='clock_display_settings.normal_clock.action'
type='select'
@@ -92,12 +86,14 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ
<SettingSeparator/>
<SettingOption
action={onSelectClockPreference}
label={intl.formatMessage({id: 'settings_display.clock.military', defaultMessage: '24-hour clock (example: 16:00)'})}
label={intl.formatMessage({id: 'settings_display.clock.mz', defaultMessage: '24-hour clock'})}
description={intl.formatMessage({id: 'settings_display.clock.mz.desc', defaultMessage: 'Example: 16:00'})}
selected={isMilitaryTimeFormat}
testID='clock_display_settings.military_clock.action'
type='select'
value={CLOCK_TYPE.MILITARY}
/>
<SettingSeparator/>
</SettingBlock>
</SettingContainer>
);

View File

@@ -3,40 +3,33 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {useTheme} from '@context/theme';
import SettingSeparator from '@screens/settings/settings_separator';
import SettingBlock from '../setting_block';
import SettingOption from '../setting_option';
const styles = StyleSheet.create({
containerStyles: {
paddingHorizontal: 16,
},
});
const radioItemProps = {checkedBody: true};
type CustomThemeProps = {
customTheme: Theme;
setTheme: (themeKey: string) => void;
displayTheme: string | undefined;
}
const CustomTheme = ({customTheme, setTheme}: CustomThemeProps) => {
const CustomTheme = ({setTheme, displayTheme}: CustomThemeProps) => {
const intl = useIntl();
const theme = useTheme();
return (
<SettingBlock
containerStyles={styles.containerStyles}
disableHeader={true}
>
<>
<SettingSeparator isGroupSeparator={true}/>
<SettingOption
action={setTheme}
type='select'
value={customTheme.type}
value={theme.type}
label={intl.formatMessage({id: 'settings_display.custom_theme', defaultMessage: 'Custom Theme'})}
selected={theme.type?.toLowerCase() === customTheme.type?.toLowerCase()}
selected={theme.type?.toLowerCase() === displayTheme?.toLowerCase()}
radioItemProps={radioItemProps}
/>
</SettingBlock>
</>
);
};

View File

@@ -1,37 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {savePreference} from '@actions/remote/preference';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {popTopScreen, setButtons} from '@screens/navigation';
import {getSaveButton} from '@screens/settings/config';
import SettingContainer from '../setting_container';
import CustomTheme from './custom_theme';
import {ThemeTiles} from './theme_tiles';
const SAVE_DISPLAY_THEME_BTN_ID = 'SAVE_DISPLAY_THEME_BTN_ID';
type DisplayThemeProps = {
allowedThemeKeys: string[];
componentId: string;
currentTeamId: string;
currentUserId: string;
}
const DisplayTheme = ({allowedThemeKeys, currentTeamId, currentUserId}: DisplayThemeProps) => {
const DisplayTheme = ({allowedThemeKeys, componentId, currentTeamId, currentUserId}: DisplayThemeProps) => {
const serverUrl = useServerUrl();
const theme = useTheme();
const [customTheme, setCustomTheme] = useState<Theme|null>();
const intl = useIntl();
const initialTheme = useMemo(() => theme.type, []); // dependency array should remain empty
useEffect(() => {
if (theme.type === 'custom') {
setCustomTheme(theme);
}
}, []);
const [displayTheme, setDisplayTheme] = useState<string | undefined>(initialTheme);
const updateTheme = useCallback((selectedThemeKey: string) => {
const selectedTheme = allowedThemeKeys.find((tk) => tk === selectedThemeKey);
const saveButton = useMemo(() => getSaveButton(SAVE_DISPLAY_THEME_BTN_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]);
const close = () => popTopScreen(componentId);
const updateTheme = useCallback(() => {
const selectedTheme = allowedThemeKeys.find((tk) => tk === displayTheme);
if (!selectedTheme) {
return;
}
@@ -42,18 +50,34 @@ const DisplayTheme = ({allowedThemeKeys, currentTeamId, currentUserId}: DisplayT
value: JSON.stringify(Preferences.THEMES[selectedTheme]),
};
savePreference(serverUrl, [pref]);
}, [serverUrl, allowedThemeKeys, currentTeamId]);
close();
}, [serverUrl, allowedThemeKeys, currentTeamId, displayTheme]);
useEffect(() => {
const buttons = {
rightButtons: [{
...saveButton,
enabled: initialTheme?.toLowerCase() !== displayTheme?.toLowerCase(),
}],
};
setButtons(componentId, buttons);
}, [componentId, saveButton, displayTheme, initialTheme]);
useNavButtonPressed(SAVE_DISPLAY_THEME_BTN_ID, componentId, updateTheme, [updateTheme]);
useAndroidHardwareBackHandler(componentId, close);
return (
<SettingContainer>
<ThemeTiles
allowedThemeKeys={allowedThemeKeys}
onThemeChange={updateTheme}
onThemeChange={setDisplayTheme}
selectedTheme={displayTheme}
/>
{customTheme && (
{theme.type === 'custom' && (
<CustomTheme
customTheme={customTheme}
setTheme={updateTheme}
setTheme={setDisplayTheme}
displayTheme={displayTheme}
/>
)}
</SettingContainer>

View File

@@ -20,12 +20,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
container: {
flexDirection: 'column',
padding: TILE_PADDING,
marginTop: 8,
},
imageWrapper: {
position: 'relative',
alignItems: 'flex-start',
marginBottom: 12,
marginBottom: 8,
},
thumbnail: {
resizeMode: 'stretch',
@@ -39,6 +38,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
label: {
color: theme.centerChannelColor,
...typography('Body', 200),
textTransform: 'capitalize',
},
tilesContainer: {
marginBottom: 30,
@@ -46,10 +46,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
flexWrap: 'wrap',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
},
};
});
@@ -99,8 +95,8 @@ export const ThemeTile = ({
>
<View style={[styles.imageWrapper, layoutStyle.thumbnail]}>
<ThemeThumbnail
borderColorBase={selected ? activeTheme.sidebarTextActiveBorder : activeTheme.centerChannelBg}
borderColorMix={selected ? activeTheme.sidebarTextActiveBorder : changeOpacity(activeTheme.centerChannelColor, 0.16)}
borderColorBase={selected ? activeTheme.buttonBg : activeTheme.centerChannelBg}
borderColorMix={selected ? activeTheme.buttonBg : changeOpacity(activeTheme.centerChannelColor, 0.16)}
theme={theme}
width={layoutStyle.thumbnail.width}
/>
@@ -120,29 +116,36 @@ export const ThemeTile = ({
type ThemeTilesProps = {
allowedThemeKeys: string[];
onThemeChange: (v: string) => void;
selectedTheme: string | undefined;
}
export const ThemeTiles = ({allowedThemeKeys, onThemeChange}: ThemeTilesProps) => {
export const ThemeTiles = ({allowedThemeKeys, onThemeChange, selectedTheme}: 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}
/>
))
allowedThemeKeys.map((themeKey: string) => {
if (!Preferences.THEMES[themeKey] || !selectedTheme) {
return null;
}
return (
<ThemeTile
key={themeKey}
label={(
<Text style={styles.label}>
{themeKey}
</Text>
)}
action={onThemeChange}
actionValue={themeKey}
selected={selectedTheme?.toLowerCase() === themeKey.toLowerCase()}
theme={Preferences.THEMES[themeKey]}
activeTheme={theme}
/>
);
})
}
</View>
);

View File

@@ -3,7 +3,6 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {View} from 'react-native';
import {updateMe} from '@actions/remote/user';
import {Screens} from '@constants';
@@ -32,9 +31,10 @@ type DisplayTimezoneProps = {
const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const timezone = useMemo(() => getUserTimezoneProps(currentUser), [currentUser.timezone]);
const [userTimezone, setUserTimezone] = useState(timezone);
const initialTimezone = useMemo(() => getUserTimezoneProps(currentUser), []); // deps array should remain empty
const [userTimezone, setUserTimezone] = useState(initialTimezone);
const theme = useTheme();
const updateAutomaticTimezone = (useAutomaticTimezone: boolean) => {
const automaticTimezone = getDeviceTimezone();
setUserTimezone((prev) => ({
@@ -55,8 +55,9 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => {
const goToSelectTimezone = preventDoubleTap(() => {
const screen = Screens.SETTINGS_DISPLAY_TIMEZONE_SELECT;
const title = intl.formatMessage({id: 'settings_display.timezone.select', defaultMessage: 'Select Timezone'});
const passProps = {
selectedTimezone: userTimezone.manualTimezone || timezone.manualTimezone || timezone.automaticTimezone,
currentTimezone: userTimezone.manualTimezone || initialTimezone.manualTimezone || initialTimezone.automaticTimezone,
onBack: updateManualTimezone,
};
@@ -80,9 +81,9 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => {
useEffect(() => {
const enabled =
timezone.useAutomaticTimezone !== userTimezone.useAutomaticTimezone ||
timezone.automaticTimezone !== userTimezone.automaticTimezone ||
timezone.manualTimezone !== userTimezone.manualTimezone;
initialTimezone.useAutomaticTimezone !== userTimezone.useAutomaticTimezone ||
initialTimezone.automaticTimezone !== userTimezone.automaticTimezone ||
initialTimezone.manualTimezone !== userTimezone.manualTimezone;
const buttons = {
rightButtons: [{
@@ -97,28 +98,35 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => {
useAndroidHardwareBackHandler(componentId, close);
const toggleDesc = useMemo(() => {
if (userTimezone.useAutomaticTimezone) {
return getTimezoneRegion(userTimezone.automaticTimezone);
}
return intl.formatMessage({id: 'settings_display.timezone.off', defaultMessage: 'Off'});
}, [userTimezone.useAutomaticTimezone]);
return (
<SettingContainer>
<SettingSeparator/>
<SettingOption
action={updateAutomaticTimezone}
description={getTimezoneRegion(userTimezone.automaticTimezone)}
description={toggleDesc}
label={intl.formatMessage({id: 'settings_display.timezone.automatically', defaultMessage: 'Set automatically'})}
selected={userTimezone.useAutomaticTimezone}
type='toggle'
/>
<SettingSeparator/>
{!userTimezone.useAutomaticTimezone && (
<View>
<SettingSeparator/>
<>
{/* <SettingSeparator/> */}
<SettingOption
action={goToSelectTimezone}
description={getTimezoneRegion(userTimezone.manualTimezone)}
info={getTimezoneRegion(userTimezone.manualTimezone)}
label={intl.formatMessage({id: 'settings_display.timezone.manual', defaultMessage: 'Change timezone'})}
type='arrow'
/>
</View>
</>
)}
<SettingSeparator/>
{/* <SettingSeparator/> */}
</SettingContainer>
);
};

View File

@@ -3,14 +3,17 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, View} from 'react-native';
import {FlatList} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import {getAllSupportedTimezones} from '@actions/remote/user';
import Search from '@components/search';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {popTopScreen} from '@screens/navigation';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {popTopScreen, setButtons} from '@screens/navigation';
import {getSaveButton} from '@screens/settings/config';
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {getTimezoneRegion} from '@utils/user';
@@ -30,15 +33,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
...typography('Body', 100, 'Regular'),
},
searchBar: {
height: 38,
marginVertical: 5,
marginBottom: 32,
},
inputContainerStyle: {
searchBarInputContainerStyle: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
height: 38,
},
searchBarContainerStyle: {
paddingHorizontal: 12,
marginLeft: 12,
marginBottom: 32,
marginTop: 12,
},
};
@@ -47,7 +48,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const EDGES: Edge[] = ['left', 'right'];
const EMPTY_TIMEZONES: string[] = [];
const ITEM_HEIGHT = 48;
const SAVE_DISPLAY_TZ_BTN_ID = 'SAVE_DISPLAY_TZ_BTN_ID';
const keyExtractor = (item: string) => item;
const getItemLayout = (_data: string[], index: number) => ({
length: ITEM_HEIGHT,
@@ -56,15 +57,16 @@ const getItemLayout = (_data: string[], index: number) => ({
});
type SelectTimezonesProps = {
selectedTimezone: string;
componentId: string;
onBack: (tz: string) => void;
currentTimezone: string;
}
const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
const SelectTimezones = ({componentId, onBack, currentTimezone}: SelectTimezonesProps) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const initialTimezones = useMemo(() => currentTimezone, []);
const cancelButtonProps = useMemo(() => ({
buttonTextStyle: {
color: changeOpacity(theme.centerChannelColor, 0.64),
@@ -77,17 +79,17 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
const [timezones, setTimezones] = useState<string[]>(EMPTY_TIMEZONES);
const [initialScrollIndex, setInitialScrollIndex] = useState<number|undefined>();
const [value, setValue] = useState('');
const [searchRegion, setSearchRegion] = useState<string|undefined>(undefined);
const [manualTimezone, setManualTimezone] = useState(currentTimezone);
const filteredTimezones = (timezonePrefix: string) => {
if (timezonePrefix.length === 0) {
const filteredTimezones = useCallback(() => {
if (!searchRegion) {
return timezones;
}
const lowerCasePrefix = timezonePrefix.toLowerCase();
const lowerCasePrefix = searchRegion.toLowerCase();
// if initial scroll index is set when the items change
// and the index is grater than the amount of items
// and the index is greater than the amount of items
// the list starts to render partial results until there is
// and interaction, so setting the index as undefined corrects
// the rendering
@@ -99,21 +101,27 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
getTimezoneRegion(t).toLowerCase().indexOf(lowerCasePrefix) >= 0 ||
t.toLowerCase().indexOf(lowerCasePrefix) >= 0
));
};
}, [searchRegion, timezones, initialScrollIndex]);
const onPressTimezone = useCallback((tzne: string) => {
onBack(tzne);
popTopScreen();
}, [onBack]);
const onPressTimezone = useCallback((tz: string) => {
setManualTimezone(tz);
}, []);
const renderItem = ({item: timezone}: {item: string}) => {
const renderItem = useCallback(({item: timezone}: {item: string}) => {
return (
<TimezoneRow
isSelected={timezone === manualTimezone}
onPressTimezone={onPressTimezone}
selectedTimezone={selectedTimezone}
timezone={timezone}
/>
);
}, [manualTimezone, onPressTimezone]);
const saveButton = useMemo(() => getSaveButton(SAVE_DISPLAY_TZ_BTN_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]);
const close = () => {
onBack(manualTimezone);
popTopScreen(componentId);
};
useEffect(() => {
@@ -122,7 +130,7 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
const allTzs = await getAllSupportedTimezones(serverUrl);
if (allTzs.length > 0) {
setTimezones(allTzs);
const timezoneIndex = allTzs.findIndex((timezone) => timezone === selectedTimezone);
const timezoneIndex = allTzs.findIndex((timezone) => timezone === currentTimezone);
if (timezoneIndex > 0) {
setInitialScrollIndex(timezoneIndex);
}
@@ -131,29 +139,44 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
getSupportedTimezones();
}, []);
useEffect(() => {
const buttons = {
rightButtons: [{
...saveButton,
enabled: initialTimezones !== manualTimezone,
}],
};
setButtons(componentId, buttons);
}, [componentId, saveButton, initialTimezones, manualTimezone]);
useNavButtonPressed(SAVE_DISPLAY_TZ_BTN_ID, componentId, close, [manualTimezone]);
useAndroidHardwareBackHandler(componentId, close);
return (
<SafeAreaView
edges={EDGES}
style={styles.container}
testID='settings.select_timezone.screen'
>
<View style={styles.searchBar}>
<Search
autoCapitalize='none'
cancelButtonProps={cancelButtonProps}
inputContainerStyle={styles.inputContainerStyle}
inputStyle={styles.searchBarInput}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onChangeText={setValue}
placeholder={intl.formatMessage({id: 'search_bar.search.placeholder', defaultMessage: 'Search timezone'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
selectionColor={changeOpacity(theme.centerChannelColor, 0.5)}
testID='settings.select_timezone.search_bar'
value={value}
/>
</View>
<Search
autoCapitalize='none'
cancelButtonProps={cancelButtonProps}
inputContainerStyle={styles.searchBarInputContainerStyle}
containerStyle={styles.searchBarContainerStyle}
inputStyle={styles.searchBarInput}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onChangeText={setSearchRegion}
placeholder={intl.formatMessage({id: 'search_bar.search.placeholder', defaultMessage: 'Search timezone'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
selectionColor={changeOpacity(theme.centerChannelColor, 0.5)}
testID='settings.select_timezone.search_bar'
value={searchRegion}
/>
<FlatList
data={filteredTimezones(value)}
contentContainerStyle={styles.flexGrow}
data={searchRegion?.length ? filteredTimezones() : timezones}
extraData={manualTimezone}
getItemLayout={getItemLayout}
initialScrollIndex={initialScrollIndex}
keyExtractor={keyExtractor}
@@ -161,7 +184,6 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
keyboardShouldPersistTaps='always'
removeClippedSubviews={true}
renderItem={renderItem}
contentContainerStyle={styles.flexGrow}
/>
</SafeAreaView>
);

View File

@@ -38,11 +38,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
};
});
type TimezoneRowProps = {
isSelected: boolean;
onPressTimezone: (timezone: string) => void;
selectedTimezone: string;
timezone: string;
}
const TimezoneRow = ({onPressTimezone, selectedTimezone, timezone}: TimezoneRowProps) => {
const TimezoneRow = ({onPressTimezone, isSelected, timezone}: TimezoneRowProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
@@ -64,7 +64,7 @@ const TimezoneRow = ({onPressTimezone, selectedTimezone, timezone}: TimezoneRowP
{timezone}
</Text>
</View>
{timezone === selectedTimezone && (
{isSelected && (
<CompassIcon
color={theme.linkColor}
name='check'

View File

@@ -52,7 +52,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
footer: {
paddingHorizontal: 20,
color: changeOpacity(theme.centerChannelColor, 0.5),
textAlign: 'justify',
...typography('Body', 75, 'Regular'),
marginTop: 20,
},

View File

@@ -41,11 +41,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
containerStyle: {
marginTop: 30,
width: '90%',
alignSelf: 'center',
paddingHorizontal: 18.5,
},
keywordLabelStyle: {
marginLeft: 20,
paddingHorizontal: 18.5,
marginTop: 4,
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 75, 'Regular'),

View File

@@ -3,6 +3,7 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import {updateMe} from '@actions/remote/user';
import {useServerUrl} from '@context/server';
@@ -10,6 +11,7 @@ import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {popTopScreen, setButtons} from '@screens/navigation';
import SettingSeparator from '@screens/settings/settings_separator';
import {getNotificationProps} from '@utils/user';
import {getSaveButton} from '../config';
@@ -88,12 +90,14 @@ const NotificationPush = ({componentId, currentUser, isCRTEnabled, sendPushNotif
sendPushNotifications={sendPushNotifications}
setMobilePushPref={setPushSend}
/>
{Platform.OS === 'android' && (<SettingSeparator isGroupSeparator={true}/>)}
{isCRTEnabled && pushSend === 'mention' && (
<MobilePushThread
pushThread={pushThread}
onMobilePushThreadChanged={onMobilePushThreadChanged}
/>
)}
{Platform.OS === 'android' && (<SettingSeparator isGroupSeparator={true}/>)}
{sendPushNotifications && pushSend !== 'none' && (
<MobilePushStatus
pushStatus={pushStatus}

View File

@@ -56,7 +56,7 @@ const MobileSendPush = ({sendPushNotifications, pushStatus, setMobilePushPref}:
<SettingSeparator/>
<SettingOption
action={setMobilePushPref}
label={intl.formatMessage({id: 'notification_settings.pushNotification.mentions.only', defaultMessage: 'Mentions, direct messages only(default)'})}
label={intl.formatMessage({id: 'notification_settings.pushNotification.mentions_only', defaultMessage: 'Mentions, direct messages only (default)'})}
selected={pushStatus === 'mention'}
testID='notification_settings.pushNotification.onlyMentions'
type='select'
@@ -71,6 +71,7 @@ const MobileSendPush = ({sendPushNotifications, pushStatus, setMobilePushPref}:
type='select'
value='none'
/>
<SettingSeparator/>
</>
}
{!sendPushNotifications &&

View File

@@ -49,6 +49,7 @@ const MobilePushStatus = ({pushStatus, setMobilePushStatus}: MobilePushStatusPro
type='select'
value='offline'
/>
<SettingSeparator/>
</SettingBlock>
);
};

View File

@@ -3,7 +3,6 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {t} from '@i18n';
@@ -11,19 +10,9 @@ import SettingBlock from '../setting_block';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
const styles = StyleSheet.create({
area: {
paddingHorizontal: 16,
},
});
const headerText = {
id: t('notification_settings.push_threads'),
defaultMessage: 'Thread reply notifications',
};
const footerText = {
id: t('notification_settings.push_threads.info'),
defaultMessage: 'When enabled, any reply to a thread you\'re following will send a mobile push notification',
id: t('notification_settings.push_threads.replies'),
defaultMessage: 'Thread replies',
};
type MobilePushThreadProps = {
@@ -37,12 +26,10 @@ const MobilePushThread = ({pushThread, onMobilePushThreadChanged}: MobilePushThr
return (
<SettingBlock
headerText={headerText}
footerText={footerText}
containerStyles={styles.area}
>
<SettingOption
action={onMobilePushThreadChanged}
label={intl.formatMessage({id: 'notification_settings.push_threads.description', defaultMessage: 'Notify me about all replies to threads I\'m following'})}
label={intl.formatMessage({id: 'notification_settings.push_threads.following', defaultMessage: 'Notify me about replies to threads I\'m following in this channel'})}
selected={pushThread === 'all'}
type='toggle'
/>

View File

@@ -12,8 +12,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
blockHeader: {
color: theme.centerChannelColor,
...typography('Heading', 300, 'SemiBold'),
marginBottom: 16,
marginLeft: 18,
marginBottom: 8,
marginLeft: 20,
marginTop: 12,
},
contentContainerStyle: {
marginBottom: 0,
},
};
});
@@ -30,6 +34,7 @@ const SettingBlock = ({headerText, ...props}: SettingBlockProps) => {
<Block
headerText={headerText}
headerStyles={styles.blockHeader}
containerStyles={styles.contentContainerStyle}
{...props}
>

View File

@@ -17,7 +17,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg,
},
contentContainerStyle: {
marginTop: 20,
marginTop: 8,
},
};
});

View File

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Platform} from 'react-native';
import OptionItem, {OptionItemProps} from '@components/option_item';
import {useTheme} from '@context/theme';
@@ -29,12 +30,15 @@ const SettingOption = ({...props}: OptionItemProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const useRadioButton = props.type === 'select' && Platform.OS === 'android';
return (
<OptionItem
optionDescriptionTextStyle={styles.optionDescriptionTextStyle}
optionLabelTextStyle={styles.optionLabelTextStyle}
containerStyle={[styles.container, props.description && {marginTop: 16}]}
containerStyle={[styles.container, props.description && {marginVertical: 12}]}
{...props}
type={useRadioButton ? 'radio' : props.type}
/>
);
};

View File

@@ -2,16 +2,12 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Platform, Text} from 'react-native';
import {Platform, StyleProp, Text, TextStyle} from 'react-native';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type SettingRowLabelProps = {
text: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
rightLabel: {
@@ -27,13 +23,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
};
});
const SettingRowLabel = ({text}: SettingRowLabelProps) => {
type SettingRowLabelProps = {
text: string;
textStyle?: StyleProp<TextStyle>;
}
const SettingRowLabel = ({text, textStyle}: SettingRowLabelProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<Text
style={styles.rightLabel}
style={[styles.rightLabel, textStyle]}
>
{text}
</Text>

View File

@@ -25,14 +25,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
containerStyle: {
paddingLeft: 8,
marginTop: 20,
marginTop: 12,
},
helpGroup: {
width: '91%',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
height: 1,
alignSelf: 'center',
marginTop: 20,
// marginTop: 20,
},
};
});
@@ -120,7 +121,7 @@ const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) =>
});
return (
<SettingContainer >
<SettingContainer>
<SettingItem
onPress={goToNotifications}
optionName='notification'

View File

@@ -8,32 +8,39 @@ import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const groupSeparator = {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.12),
width: '91%',
alignSelf: 'center',
height: 1,
};
return {
separator: {
...Platform.select({
ios: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
width: '91%',
alignSelf: 'center',
height: 1,
marginTop: 12,
...groupSeparator,
},
default: {
display: 'none',
},
}),
},
groupSeparator: {
...groupSeparator,
marginBottom: 16,
},
};
});
type SettingSeparatorProps = {
lineStyles?: StyleProp<ViewStyle>;
isGroupSeparator?: boolean;
}
const SettingSeparator = ({lineStyles}: SettingSeparatorProps) => {
const SettingSeparator = ({lineStyles, isGroupSeparator = false}: SettingSeparatorProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (<View style={[styles.separator, lineStyles]}/>);
return (<View style={[styles.separator, isGroupSeparator && styles.groupSeparator, lineStyles]}/>);
};
export default SettingSeparator;

View File

@@ -347,10 +347,6 @@
"mentions.empty.paragraph": "You'll see messages here when someone mentions you or uses terms you're monitoring.",
"mentions.empty.title": "No Mentions yet",
"mobile.about.appVersion": "App Version: {version} (Build {number})",
"mobile.about.copyright": "Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved",
"mobile.about.database": "Database: {type}",
"mobile.about.licensed": "Licensed to: {company}",
"mobile.about.powered_by": "{site} is powered by Mattermost",
"mobile.about.serverVersion": "Server Version: {version} (Build {number})",
"mobile.about.serverVersionNoBuild": "Server Version: {version}",
"mobile.account.settings.save": "Save",
@@ -454,9 +450,6 @@
"mobile.no_results_with_term.files": "No files matching “{term}”",
"mobile.no_results_with_term.messages": "No matches found for “{term}”",
"mobile.no_results.spelling": "Check the spelling or try another search.",
"mobile.notice_mobile_link": "mobile apps",
"mobile.notice_platform_link": "server",
"mobile.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",
"mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.",
"mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.",
"mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify that a browser is installed on the device.",
@@ -608,12 +601,11 @@
"notification_settings.mobile.trigger_push": "Trigger push notifications when...",
"notification_settings.ooo_auto_responder": "Automatic replies",
"notification_settings.push_notification": "Push Notifications",
"notification_settings.push_threads": "Thread reply notifications",
"notification_settings.push_threads.description": "Notify me about all replies to threads I'm following",
"notification_settings.push_threads.info": "When enabled, any reply to a thread you're following will send a mobile push notification",
"notification_settings.push_threads.following": "Notify me about replies to threads I'm following in this channel",
"notification_settings.push_threads.replies": "Thread replies",
"notification_settings.pushNotification.all_new_messages": "All new messages",
"notification_settings.pushNotification.disabled_long": "Push notifications for mobile devices have been disabled by your System Administrator.",
"notification_settings.pushNotification.mentions.only": "Mentions, direct messages only(default)",
"notification_settings.pushNotification.mentions_only": "Mentions, direct messages only (default)",
"notification_settings.pushNotification.nothing": "Nothing",
"notification_settings.send_notification.about": "Notify me about...",
"notification_settings.threads_mentions": "Mentions in threads",
@@ -722,16 +714,31 @@
"servers.login": "Log in",
"servers.logout": "Log out",
"servers.remove": "Remove",
"settings_display.clock.military": "24-hour clock (example: 16:00)",
"settings_display.clock.normal": "12-hour clock (example: 4:00 PM)",
"settings_display.clock.preferTime": "Select how you prefer time displayed.",
"settings_display.clock.mz": "24-hour clock",
"settings_display.clock.mz.desc": "Example: 16:00",
"settings_display.clock.normal.desc": "Example: 4:00 PM",
"settings_display.clock.standard": "12-hour clock",
"settings_display.custom_theme": "Custom Theme",
"settings_display.timezone.automatically": "Set automatically",
"settings_display.timezone.manual": "Change timezone",
"settings_display.timezone.off": "Off",
"settings_display.timezone.select": "Select Timezone",
"settings.about": "About {appTitle}",
"settings.about.build": "{version} (Build {number})",
"settings.about.copyright": "Copyright 2015-{currentYear} Mattermost, Inc. All rights reserved",
"settings.about.database": "Database:",
"settings.about.licensed": "Licensed to: {company}",
"settings.about.powered_by": "{site} is powered by Mattermost",
"settings.about.serverVersion": "{version} (Build {number})",
"settings.about.serverVersionNoBuild": "{version}",
"settings.about.version": "App Version:",
"settings.advanced_settings": "Advanced Settings",
"settings.display": "Display",
"settings.link.error.text": "Unable to open the link.",
"settings.link.error.title": "Error",
"settings.notice_mobile_link": "mobile apps",
"settings.notice_platform_link": "server",
"settings.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",
"settings.notifications": "Notifications",
"settings.save": "Save",
"smobile.search.recent_title": "Recent searches in {teamName}",