diff --git a/app/components/menu_item/__snapshots__/index.test.tsx.snap b/app/components/menu_item/__snapshots__/index.test.tsx.snap index 3850e17316..6fde1aafa6 100644 --- a/app/components/menu_item/__snapshots__/index.test.tsx.snap +++ b/app/components/menu_item/__snapshots__/index.test.tsx.snap @@ -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, diff --git a/app/components/menu_item/index.tsx b/app/components/menu_item/index.tsx index 44d43c6643..345ba9ba7a 100644 --- a/app/components/menu_item/index.tsx +++ b/app/components/menu_item/index.tsx @@ -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: { diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx index a4e8a27435..3220b490a8 100644 --- a/app/components/option_item/index.tsx +++ b/app/components/option_item/index.tsx @@ -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>)|((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; - optionLabelTextStyle?: StyleProp; - optionDescriptionTextStyle?: StyleProp; -} +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>)|((value: string | boolean) => void); + containerStyle?: StyleProp; + description?: string; + destructive?: boolean; + icon?: string; + info?: string; + inline?: boolean; + label: string; + optionDescriptionTextStyle?: StyleProp; + optionLabelTextStyle?: StyleProp; + radioItemProps?: Partial; + 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 = ( ); + } else if (type === OptionType.RADIO) { + radioComponent = ( + + ); } 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 = ({ /> )} + {type === OptionType.RADIO && radioComponent} ); - if (type === OptionType.DEFAULT || type === OptionType.SELECT || type === OptionType.ARROW) { + if (type === OptionType.DEFAULT || type === OptionType.SELECT || type === OptionType.ARROW || type === OptionType.RADIO) { return ( {component} diff --git a/app/components/option_item/radio_item.tsx b/app/components/option_item/radio_item.tsx new file mode 100644 index 0000000000..d79b6a4191 --- /dev/null +++ b/app/components/option_item/radio_item.tsx @@ -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 ( + + + + ); + } + + return (); + }, [checkedBody]); + + return ( + + {selected && getBody()} + + ); +}; + +export default RadioItem; diff --git a/app/screens/about/index.tsx b/app/screens/about/index.tsx deleted file mode 100644 index 2db27425d6..0000000000 --- a/app/screens/about/index.tsx +++ /dev/null @@ -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 ( - - - - - - - - - {`${config.SiteName} `} - - - </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)); diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 09943a68d0..baa707d88d 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -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); diff --git a/app/screens/settings/about/about.tsx b/app/screens/settings/about/about.tsx new file mode 100644 index 0000000000..68a41c8023 --- /dev/null +++ b/app/screens/settings/about/about.tsx @@ -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; diff --git a/app/screens/settings/about/index.tsx b/app/screens/settings/about/index.tsx new file mode 100644 index 0000000000..b2985f9bdd --- /dev/null +++ b/app/screens/settings/about/index.tsx @@ -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)); diff --git a/app/screens/about/learn_more.tsx b/app/screens/settings/about/learn_more.tsx similarity index 93% rename from app/screens/about/learn_more.tsx rename to app/screens/settings/about/learn_more.tsx index ee6b9175cd..ccd69d713e 100644 --- a/app/screens/about/learn_more.tsx +++ b/app/screens/settings/about/learn_more.tsx @@ -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; diff --git a/app/screens/about/subtitle.tsx b/app/screens/settings/about/subtitle.tsx similarity index 77% rename from app/screens/about/subtitle.tsx rename to app/screens/settings/about/subtitle.tsx index a6cfa00a38..d7bbd23305 100644 --- a/app/screens/about/subtitle.tsx +++ b/app/screens/settings/about/subtitle.tsx @@ -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; diff --git a/app/screens/about/title.tsx b/app/screens/settings/about/title.tsx similarity index 60% rename from app/screens/about/title.tsx rename to app/screens/settings/about/title.tsx index 9b5771010b..f5a3dbc48a 100644 --- a/app/screens/about/title.tsx +++ b/app/screens/settings/about/title.tsx @@ -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; diff --git a/app/screens/about/tos_privacy.tsx b/app/screens/settings/about/tos_privacy.tsx similarity index 100% rename from app/screens/about/tos_privacy.tsx rename to app/screens/settings/about/tos_privacy.tsx diff --git a/app/screens/settings/advanced/index.tsx b/app/screens/settings/advanced/index.tsx index c535018cf6..094b3ffcb3 100644 --- a/app/screens/settings/advanced/index.tsx +++ b/app/screens/settings/advanced/index.tsx @@ -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> ); diff --git a/app/screens/settings/display/display.tsx b/app/screens/settings/display/display.tsx index ea9552a245..f2d0ac97b1 100644 --- a/app/screens/settings/display/display.tsx +++ b/app/screens/settings/display/display.tsx @@ -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} /> } /> diff --git a/app/screens/settings/display_clock/display_clock.tsx b/app/screens/settings/display_clock/display_clock.tsx index 1e94d18a4a..c9f3143c7e 100644 --- a/app/screens/settings/display_clock/display_clock.tsx +++ b/app/screens/settings/display_clock/display_clock.tsx @@ -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> ); diff --git a/app/screens/settings/display_theme/custom_theme.tsx b/app/screens/settings/display_theme/custom_theme.tsx index fb801e158d..75a2eeb90a 100644 --- a/app/screens/settings/display_theme/custom_theme.tsx +++ b/app/screens/settings/display_theme/custom_theme.tsx @@ -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> + </> ); }; diff --git a/app/screens/settings/display_theme/display_theme.tsx b/app/screens/settings/display_theme/display_theme.tsx index 732189e2db..0712a4804c 100644 --- a/app/screens/settings/display_theme/display_theme.tsx +++ b/app/screens/settings/display_theme/display_theme.tsx @@ -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> diff --git a/app/screens/settings/display_theme/theme_tiles.tsx b/app/screens/settings/display_theme/theme_tiles.tsx index 65a887146b..a9e3134af8 100644 --- a/app/screens/settings/display_theme/theme_tiles.tsx +++ b/app/screens/settings/display_theme/theme_tiles.tsx @@ -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> ); diff --git a/app/screens/settings/display_timezone/display_timezone.tsx b/app/screens/settings/display_timezone/display_timezone.tsx index cb7999c109..ce07bda051 100644 --- a/app/screens/settings/display_timezone/display_timezone.tsx +++ b/app/screens/settings/display_timezone/display_timezone.tsx @@ -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> ); }; diff --git a/app/screens/settings/display_timezone_select/index.tsx b/app/screens/settings/display_timezone_select/index.tsx index 6d60e978c6..993733b3f2 100644 --- a/app/screens/settings/display_timezone_select/index.tsx +++ b/app/screens/settings/display_timezone_select/index.tsx @@ -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> ); diff --git a/app/screens/settings/display_timezone_select/timezone_row.tsx b/app/screens/settings/display_timezone_select/timezone_row.tsx index 92579609ff..99ed738816 100644 --- a/app/screens/settings/display_timezone_select/timezone_row.tsx +++ b/app/screens/settings/display_timezone_select/timezone_row.tsx @@ -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' diff --git a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx index d99a9fb23d..e7f29cadfd 100644 --- a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx +++ b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx @@ -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, }, diff --git a/app/screens/settings/notification_mention/mention_settings.tsx b/app/screens/settings/notification_mention/mention_settings.tsx index 2612d15dc3..20aeee5a68 100644 --- a/app/screens/settings/notification_mention/mention_settings.tsx +++ b/app/screens/settings/notification_mention/mention_settings.tsx @@ -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'), diff --git a/app/screens/settings/notification_push/notification_push.tsx b/app/screens/settings/notification_push/notification_push.tsx index dd337dec23..e188450337 100644 --- a/app/screens/settings/notification_push/notification_push.tsx +++ b/app/screens/settings/notification_push/notification_push.tsx @@ -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} diff --git a/app/screens/settings/notification_push/push_send.tsx b/app/screens/settings/notification_push/push_send.tsx index 36eaf5cce4..5eedc91f9a 100644 --- a/app/screens/settings/notification_push/push_send.tsx +++ b/app/screens/settings/notification_push/push_send.tsx @@ -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 && diff --git a/app/screens/settings/notification_push/push_status.tsx b/app/screens/settings/notification_push/push_status.tsx index 0954494ecd..4559b8b047 100644 --- a/app/screens/settings/notification_push/push_status.tsx +++ b/app/screens/settings/notification_push/push_status.tsx @@ -49,6 +49,7 @@ const MobilePushStatus = ({pushStatus, setMobilePushStatus}: MobilePushStatusPro type='select' value='offline' /> + <SettingSeparator/> </SettingBlock> ); }; diff --git a/app/screens/settings/notification_push/push_thread.tsx b/app/screens/settings/notification_push/push_thread.tsx index 70461967d4..b3f0c88be9 100644 --- a/app/screens/settings/notification_push/push_thread.tsx +++ b/app/screens/settings/notification_push/push_thread.tsx @@ -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' /> diff --git a/app/screens/settings/setting_block.tsx b/app/screens/settings/setting_block.tsx index ba5e2c0371..b46e52c93b 100644 --- a/app/screens/settings/setting_block.tsx +++ b/app/screens/settings/setting_block.tsx @@ -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} > diff --git a/app/screens/settings/setting_container.tsx b/app/screens/settings/setting_container.tsx index a4af358d09..b5f98ce32c 100644 --- a/app/screens/settings/setting_container.tsx +++ b/app/screens/settings/setting_container.tsx @@ -17,7 +17,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { backgroundColor: theme.centerChannelBg, }, contentContainerStyle: { - marginTop: 20, + marginTop: 8, }, }; }); diff --git a/app/screens/settings/setting_option.tsx b/app/screens/settings/setting_option.tsx index 794204e156..af11b4ede9 100644 --- a/app/screens/settings/setting_option.tsx +++ b/app/screens/settings/setting_option.tsx @@ -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} /> ); }; diff --git a/app/screens/settings/setting_row_label.tsx b/app/screens/settings/setting_row_label.tsx index 536ff13b88..3cdc2341cd 100644 --- a/app/screens/settings/setting_row_label.tsx +++ b/app/screens/settings/setting_row_label.tsx @@ -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> diff --git a/app/screens/settings/settings.tsx b/app/screens/settings/settings.tsx index ba66b42993..83ae77238d 100644 --- a/app/screens/settings/settings.tsx +++ b/app/screens/settings/settings.tsx @@ -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' diff --git a/app/screens/settings/settings_separator.tsx b/app/screens/settings/settings_separator.tsx index 3ed118c2cb..7a4f27e4a8 100644 --- a/app/screens/settings/settings_separator.tsx +++ b/app/screens/settings/settings_separator.tsx @@ -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; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index fc586470f3..97e170d6ed 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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}",