diff --git a/app/components/block/index.tsx b/app/components/block/index.tsx index 036474f33..f293c679f 100644 --- a/app/components/block/index.tsx +++ b/app/components/block/index.tsx @@ -20,13 +20,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { fontSize: 13, color: changeOpacity(theme.centerChannelColor, 0.5), }, - items: { - backgroundColor: theme.centerChannelBg, - borderTopWidth: 1, - borderBottomWidth: 1, - borderTopColor: changeOpacity(theme.centerChannelColor, 0.1), - borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1), - }, footer: { marginTop: 10, marginHorizontal: 15, @@ -36,13 +29,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -type SectionText = { +export type SectionText = { id: string; defaultMessage: string; values?: MessageDescriptor; } -type SectionProps = { +export type BlockProps = { children: React.ReactNode; disableFooter?: boolean; disableHeader?: boolean; @@ -62,7 +55,7 @@ const Block = ({ headerStyles, headerText, footerStyles, -}: SectionProps) => { +}: BlockProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); @@ -76,7 +69,7 @@ const Block = ({ style={[styles.header, headerStyles]} /> } - + {children} {(footerText && !disableFooter) && diff --git a/app/components/common_post_options/base_option/index.tsx b/app/components/common_post_options/base_option/index.tsx index 10f534229..c5d06f426 100644 --- a/app/components/common_post_options/base_option/index.tsx +++ b/app/components/common_post_options/base_option/index.tsx @@ -52,14 +52,13 @@ const BaseOption = ({ return ( ); }; diff --git a/app/components/menu_item/__snapshots__/index.test.tsx.snap b/app/components/menu_item/__snapshots__/index.test.tsx.snap index 39d2569e3..3850e1731 100644 --- a/app/components/menu_item/__snapshots__/index.test.tsx.snap +++ b/app/components/menu_item/__snapshots__/index.test.tsx.snap @@ -110,10 +110,13 @@ exports[`DrawerItem should match snapshot 1`] = ` diff --git a/app/components/menu_item/index.tsx b/app/components/menu_item/index.tsx index 8f34bffb0..44d43c664 100644 --- a/app/components/menu_item/index.tsx +++ b/app/components/menu_item/index.tsx @@ -7,6 +7,7 @@ import {Platform, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import CompassIcon from '@components/compass_icon'; import FormattedText from '@components/formatted_text'; import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; export const ITEM_HEIGHT = 50; @@ -64,6 +65,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { export type MenuItemProps = { centered?: boolean; + chevronStyle?: StyleProp; containerStyle?: StyleProp; defaultMessage?: string; i18nId?: string; @@ -78,24 +80,34 @@ export type MenuItemProps = { onPress: () => void; rightComponent?: ReactNode; separator?: boolean; + separatorStyle?: StyleProp; showArrow?: boolean; testID: string; - theme: Theme; }; -const MenuItem = (props: MenuItemProps) => { - const { - centered, containerStyle, defaultMessage = '', i18nId, iconContainerStyle, iconName, isDestructor = false, - isLink = false, labelComponent, labelStyle, leftComponent, messageValues, onPress, rightComponent, - separator = true, showArrow = false, testID, theme, - } = props; - +const MenuItem = ({ + centered, + chevronStyle, + containerStyle, + defaultMessage = '', + i18nId, + iconContainerStyle, + iconName, + isDestructor = false, + isLink = false, + labelComponent, + labelStyle, + leftComponent, + messageValues, + onPress, + rightComponent, + separator = true, + separatorStyle, + showArrow = false, + testID, +}: MenuItemProps) => { + const theme = useTheme(); const style = getStyleSheet(theme); - const destructor: any = {}; - if (isDestructor) { - destructor.color = theme.errorTextColor; - } - let icon; if (leftComponent) { icon = leftComponent; @@ -103,7 +115,7 @@ const MenuItem = (props: MenuItemProps) => { icon = ( ); } @@ -119,7 +131,7 @@ const MenuItem = (props: MenuItemProps) => { style={[ style.label, labelStyle, - destructor, + isDestructor && {color: theme.errorTextColor}, centered ? style.centerLabel : {}, isLink && style.linkContainer, ]} @@ -144,15 +156,15 @@ const MenuItem = (props: MenuItemProps) => { {label} + {rightComponent} {Boolean(showArrow) && ( )} - {rightComponent} - {Boolean(separator) && ()} + {Boolean(separator) && ()} ); diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx index c68f176a0..a4e8a2743 100644 --- a/app/components/option_item/index.tsx +++ b/app/components/option_item/index.tsx @@ -2,15 +2,15 @@ // See LICENSE.txt for license information. import React, {useCallback, useMemo} from 'react'; -import {Platform, StyleProp, Switch, Text, TouchableOpacity, View, ViewStyle} from 'react-native'; +import {Platform, StyleProp, Switch, Text, TextStyle, TouchableOpacity, View, ViewStyle} from 'react-native'; import CompassIcon from '@components/compass_icon'; import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; -type Props = { - action?: (value: string | boolean) => void; +export type OptionItemProps = { + action?: (React.Dispatch>)|((value: string | boolean) => void) ; description?: string; inline?: boolean; destructive?: boolean; @@ -22,6 +22,8 @@ type Props = { type: OptionType; value?: string; containerStyle?: StyleProp; + optionLabelTextStyle?: StyleProp; + optionDescriptionTextStyle?: StyleProp; } const OptionType = { @@ -95,10 +97,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }); const OptionItem = ({ - action, description, destructive, icon, - info, inline = false, label, selected, - testID = 'optionItem', type, value, containerStyle, -}: Props) => { + action, + containerStyle, + description, + destructive, + icon, + info, + inline = false, + label, + optionDescriptionTextStyle, + optionLabelTextStyle, + selected, + testID = 'optionItem', + type, + value, +}: OptionItemProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); @@ -181,14 +194,14 @@ const OptionItem = ({ )} {label} {Boolean(description) && {description} @@ -199,10 +212,11 @@ const OptionItem = ({ {Boolean(actionComponent || info) && - {Boolean(info) && - - {info} - + { + Boolean(info) && + + {info} + } {actionComponent} diff --git a/app/constants/screens.ts b/app/constants/screens.ts index de537b977..afc5ff1f9 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -49,6 +49,7 @@ export const SETTINGS_DISPLAY_TIMEZONE = 'SettingsDisplayTimezone'; export const SETTINGS_DISPLAY_TIMEZONE_SELECT = 'SettingsDisplayTimezoneSelect'; export const SETTINGS_NOTIFICATION = 'SettingsNotification'; export const SETTINGS_NOTIFICATION_AUTO_RESPONDER = 'SettingsNotificationAutoResponder'; +export const SETTINGS_NOTIFICATION_EMAIL = 'SettingsNotificationEmail'; export const SETTINGS_NOTIFICATION_MENTION = 'SettingsNotificationMention'; export const SETTINGS_NOTIFICATION_PUSH = 'SettingsNotificationPush'; export const SNACK_BAR = 'SnackBar'; @@ -108,6 +109,7 @@ export default { SETTINGS_DISPLAY_TIMEZONE_SELECT, SETTINGS_NOTIFICATION, SETTINGS_NOTIFICATION_AUTO_RESPONDER, + SETTINGS_NOTIFICATION_EMAIL, SETTINGS_NOTIFICATION_MENTION, SETTINGS_NOTIFICATION_PUSH, SNACK_BAR, diff --git a/app/screens/home/account/components/options/custom_status/index.tsx b/app/screens/home/account/components/options/custom_status/index.tsx index 4e9b84582..bde92e0e2 100644 --- a/app/screens/home/account/components/options/custom_status/index.tsx +++ b/app/screens/home/account/components/options/custom_status/index.tsx @@ -88,7 +88,6 @@ const CustomStatus = ({isCustomStatusExpirySupported, isTablet, currentUser}: Cu />} separator={false} onPress={goToCustomStatusScreen} - theme={theme} /> ); }; diff --git a/app/screens/home/account/components/options/index.tsx b/app/screens/home/account/components/options/index.tsx index 6c468ceec..587b38952 100644 --- a/app/screens/home/account/components/options/index.tsx +++ b/app/screens/home/account/components/options/index.tsx @@ -89,7 +89,6 @@ const AccountOptions = ({user, enableCustomUserStatuses, isCustomStatusExpirySup { return ( { /> )} - iconName='exit-to-app' - isDestructor={true} onPress={onLogout} separator={false} - theme={theme} + testID='account.logout.action' /> ); }; diff --git a/app/screens/home/account/components/options/settings/index.tsx b/app/screens/home/account/components/options/settings/index.tsx index 5a7804e5b..e2134be8a 100644 --- a/app/screens/home/account/components/options/settings/index.tsx +++ b/app/screens/home/account/components/options/settings/index.tsx @@ -15,10 +15,9 @@ import {preventDoubleTap} from '@utils/tap'; type Props = { isTablet: boolean; style: TextStyle; - theme: Theme; } -const Settings = ({isTablet, style, theme}: Props) => { +const Settings = ({isTablet, style}: Props) => { const intl = useIntl(); const openSettings = useCallback(preventDoubleTap(() => { @@ -34,7 +33,7 @@ const Settings = ({isTablet, style, theme}: Props) => { return ( { style={style} /> } - iconName='settings-outline' onPress={openSettings} separator={false} - theme={theme} + testID='account.settings.action' /> ); }; diff --git a/app/screens/home/account/components/options/user_presence/index.tsx b/app/screens/home/account/components/options/user_presence/index.tsx index 8ece32850..f65203622 100644 --- a/app/screens/home/account/components/options/user_presence/index.tsx +++ b/app/screens/home/account/components/options/user_presence/index.tsx @@ -120,7 +120,6 @@ const UserStatus = ({currentUser, style, theme}: Props) => { return ( { status={currentUser.status} /> } - separator={false} onPress={handleSetStatus} - theme={theme} + separator={false} + testID='account.status.action' /> ); }; diff --git a/app/screens/home/account/components/options/your_profile/index.tsx b/app/screens/home/account/components/options/your_profile/index.tsx index 67db463fc..4bbdca99c 100644 --- a/app/screens/home/account/components/options/your_profile/index.tsx +++ b/app/screens/home/account/components/options/your_profile/index.tsx @@ -33,7 +33,7 @@ const YourProfile = ({isTablet, style, theme}: Props) => { return ( { style={style} /> } - iconName={ACCOUNT_OUTLINE_IMAGE} onPress={openProfile} separator={false} - theme={theme} + testID='account.your_profile.action' /> ); }; diff --git a/app/screens/home/search/modifiers/show_more.tsx b/app/screens/home/search/modifiers/show_more.tsx index 409a25222..dc84920b1 100644 --- a/app/screens/home/search/modifiers/show_more.tsx +++ b/app/screens/home/search/modifiers/show_more.tsx @@ -37,8 +37,6 @@ const ShowMoreButton = ({onPress, showMore}: ShowMoreButtonProps) => { return ( { style={style.showMore} /> } + onPress={onPress} separator={false} - theme={theme} + testID={'mobile.search.show_more'} /> ); }; diff --git a/app/screens/index.tsx b/app/screens/index.tsx index fd859f323..d3019290d 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -175,6 +175,9 @@ Navigation.setLazyComponentRegistrator((screenName) => { case Screens.SETTINGS_NOTIFICATION_AUTO_RESPONDER: screen = withServerDatabase(require('@screens/settings/notification_auto_responder').default); break; + case Screens.SETTINGS_NOTIFICATION_EMAIL: + screen = withServerDatabase(require('@screens/settings/notification_email').default); + break; case Screens.SETTINGS_NOTIFICATION_MENTION: screen = withServerDatabase(require('@screens/settings/notification_mention').default); break; diff --git a/app/screens/settings/advanced/index.tsx b/app/screens/settings/advanced/index.tsx index f56158d29..c535018cf 100644 --- a/app/screens/settings/advanced/index.tsx +++ b/app/screens/settings/advanced/index.tsx @@ -3,40 +3,31 @@ import React, {useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; -import {View, TouchableOpacity} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; +import {TouchableOpacity} from 'react-native'; -import OptionItem from '@components/option_item'; import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {popTopScreen} from '@screens/navigation'; import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file'; import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +import SettingContainer from '../setting_container'; +import SettingOption from '../setting_option'; import type {ReadDirItem} from 'react-native-fs'; const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { - screen: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - body: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - flex: 1, - paddingTop: 35, - }, itemStyle: { backgroundColor: theme.centerChannelBg, - paddingHorizontal: 8, + paddingHorizontal: 20, }, }; }); const EMPTY_FILES: ReadDirItem[] = []; const EMPTY_SERVERS: string[] = []; -const EDGES: Edge[] = ['left', 'right']; type AdvancedSettingsProps = { componentId: string; @@ -81,29 +72,22 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => { const disabled = Boolean(dataSize && (dataSize > 0)); return ( - - + - - - - - + + + ); }; diff --git a/app/screens/settings/constant.ts b/app/screens/settings/config.ts similarity index 78% rename from app/screens/settings/constant.ts rename to app/screens/settings/config.ts index d9f265c4a..4e1569a6b 100644 --- a/app/screens/settings/constant.ts +++ b/app/screens/settings/config.ts @@ -2,6 +2,19 @@ // See LICENSE.txt for license information. import {t} from '@i18n'; +import {typography} from '@utils/typography'; + +import type {IntlShape} from 'react-intl'; + +export const getSaveButton = (buttonId: string, intl: IntlShape, color: string) => ({ + color, + enabled: false, + id: buttonId, + showAsAction: 'always' as const, + testID: 'notification_settings.mentions.save.button', + text: intl.formatMessage({id: 'settings.save', defaultMessage: 'Save'}), + ...typography('Body', 100, 'SemiBold'), +}); export const SettingOptionConfig = { notification: { @@ -47,8 +60,14 @@ export const NotificationsOptionConfig = { iconName: 'cellphone', testID: 'notification_settings.push_notification', }, + email: { + defaultMessage: 'Email', + i18nId: t('notification_settings.email'), + iconName: 'email-outline', + testID: 'notification_settings.email', + }, automatic_dm_replies: { - defaultMessage: 'Automatic Direct Message Replies', + defaultMessage: 'Automatic replies', i18nId: t('notification_settings.ooo_auto_responder'), iconName: 'reply-outline', testID: 'notification_settings.automatic_dm_replies', diff --git a/app/screens/settings/display/display.tsx b/app/screens/settings/display/display.tsx index 8d53fe596..ea9552a24 100644 --- a/app/screens/settings/display/display.tsx +++ b/app/screens/settings/display/display.tsx @@ -1,49 +1,54 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {Platform, ScrollView, View} from 'react-native'; -import {SafeAreaView} from 'react-native-safe-area-context'; import {Screens} from '@constants'; import {useTheme} from '@context/theme'; +import {t} from '@i18n'; import {goToScreen} from '@screens/navigation'; -import SettingOption from '@screens/settings/setting_option'; import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {getUserTimezoneProps} from '@utils/user'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - ...Platform.select({ - ios: { - flex: 1, - paddingTop: 35, - }, - }), - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - width: '100%', - }, - }; -}); +import SettingContainer from '../setting_container'; +import SettingItem from '../setting_item'; +import SettingRowLabel from '../setting_row_label'; + +import type UserModel from '@typings/database/models/servers/user'; + +const TIME_FORMAT = [ + { + id: t('display_settings.clock.standard'), + defaultMessage: '12-hour', + }, + { + id: t('display_settings.clock.military'), + defaultMessage: '24-hour', + }, +]; + +const TIMEZONE_FORMAT = [ + { + id: t('display_settings.tz.auto'), + defaultMessage: 'Auto', + }, + { + id: t('display_settings.tz.manual'), + defaultMessage: 'Manual', + }, +]; type DisplayProps = { - isTimezoneEnabled: boolean; + currentUser: UserModel; + hasMilitaryTimeFormat: boolean; isThemeSwitchingEnabled: boolean; + isTimezoneEnabled: boolean; } -const Display = ({isTimezoneEnabled, isThemeSwitchingEnabled}: DisplayProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); +const Display = ({currentUser, hasMilitaryTimeFormat, isThemeSwitchingEnabled, isTimezoneEnabled}: DisplayProps) => { const intl = useIntl(); + const theme = useTheme(); + const timezone = useMemo(() => getUserTimezoneProps(currentUser), [currentUser.timezone]); const goToThemeSettings = preventDoubleTap(() => { const screen = Screens.SETTINGS_DISPLAY_THEME; @@ -66,35 +71,39 @@ const Display = ({isTimezoneEnabled, isThemeSwitchingEnabled}: DisplayProps) => }); return ( - - - - {isThemeSwitchingEnabled && ( - - )} - + {isThemeSwitchingEnabled && ( + + } /> - {isTimezoneEnabled && ( - - )} - - - + } + /> + {isTimezoneEnabled && ( + + } + /> + )} + ); }; diff --git a/app/screens/settings/display/index.tsx b/app/screens/settings/display/index.tsx index aab0bd82d..2ac9c35ca 100644 --- a/app/screens/settings/display/index.tsx +++ b/app/screens/settings/display/index.tsx @@ -6,7 +6,11 @@ import withObservables from '@nozbe/with-observables'; import {combineLatest, of as of$} from 'rxjs'; import {switchMap} from 'rxjs/operators'; +import {Preferences} from '@constants'; +import {getPreferenceAsBool} from '@helpers/api/preference'; +import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {observeAllowedThemesKeys, observeConfigBooleanValue} from '@queries/servers/system'; +import {observeCurrentUser} from '@queries/servers/user'; import DisplaySettings from './display'; @@ -27,6 +31,13 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { return { isTimezoneEnabled, isThemeSwitchingEnabled, + hasMilitaryTimeFormat: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS). + observeWithColumns(['value']).pipe( + switchMap( + (preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)), + ), + ), + currentUser: observeCurrentUser(database), }; }); diff --git a/app/screens/settings/display_clock/display_clock.tsx b/app/screens/settings/display_clock/display_clock.tsx index 66c88913f..1e94d18a4 100644 --- a/app/screens/settings/display_clock/display_clock.tsx +++ b/app/screens/settings/display_clock/display_clock.tsx @@ -3,12 +3,8 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; import {savePreference} from '@actions/remote/preference'; -import Block from '@components/block'; -import OptionItem from '@components/option_item'; import {Preferences} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; @@ -16,40 +12,23 @@ 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 {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import {getSaveButton} from '../config'; +import SettingBlock from '../setting_block'; +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 edges: Edge[] = ['left', 'right']; const CLOCK_TYPE = { NORMAL: 'NORMAL', MILITARY: 'MILITARY', } as const; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - flex: 1, - paddingTop: 35, - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - }, - containerStyle: { - paddingHorizontal: 8, - }, - }; -}); - const SAVE_CLOCK_BUTTON_ID = 'settings_display.clock.save.button'; type DisplayClockProps = { @@ -63,18 +42,7 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ const serverUrl = useServerUrl(); const intl = useIntl(); - const styles = getStyleSheet(theme); - - const saveButton = useMemo(() => { - return { - id: SAVE_CLOCK_BUTTON_ID, - enabled: false, - showAsAction: 'always' as const, - testID: 'settings_display.save.button', - color: theme.sidebarHeaderTextColor, - text: intl.formatMessage({id: 'settings.display.militaryClock.save', defaultMessage: 'Save'}), - }; - }, [theme.sidebarHeaderTextColor]); + const saveButton = useMemo(() => getSaveButton(SAVE_CLOCK_BUTTON_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]); const onSelectClockPreference = useCallback((clockType: keyof typeof CLOCK_TYPE) => { setIsMilitaryTimeFormat(clockType === CLOCK_TYPE.MILITARY); @@ -108,38 +76,30 @@ const DisplayClock = ({componentId, currentUserId, hasMilitaryTimeFormat}: Displ useNavButtonPressed(SAVE_CLOCK_BUTTON_ID, componentId, saveClockDisplayPreference, [isMilitaryTimeFormat]); return ( - - - - - - - - - + + + + + + + ); }; diff --git a/app/screens/settings/display_theme/custom_theme.tsx b/app/screens/settings/display_theme/custom_theme.tsx index b6d2f783d..fb801e158 100644 --- a/app/screens/settings/display_theme/custom_theme.tsx +++ b/app/screens/settings/display_theme/custom_theme.tsx @@ -3,23 +3,17 @@ import React from 'react'; import {useIntl} from 'react-intl'; +import {StyleSheet} from 'react-native'; -import Block from '@components/block'; -import OptionItem from '@components/option_item'; import {useTheme} from '@context/theme'; -import {makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - label: { - color: theme.centerChannelColor, - ...typography('Body', 200), - }, - containerStyles: { - paddingHorizontal: 16, - }, - }; +import SettingBlock from '../setting_block'; +import SettingOption from '../setting_option'; + +const styles = StyleSheet.create({ + containerStyles: { + paddingHorizontal: 16, + }, }); type CustomThemeProps = { @@ -28,23 +22,21 @@ type CustomThemeProps = { } const CustomTheme = ({customTheme, setTheme}: CustomThemeProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); const intl = useIntl(); - + const theme = useTheme(); return ( - - - + ); }; diff --git a/app/screens/settings/display_theme/display_theme.tsx b/app/screens/settings/display_theme/display_theme.tsx index 8ce40600c..732189e2d 100644 --- a/app/screens/settings/display_theme/display_theme.tsx +++ b/app/screens/settings/display_theme/display_theme.tsx @@ -2,30 +2,17 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useState} from 'react'; -import {ScrollView, View} from 'react-native'; import {savePreference} from '@actions/remote/preference'; import {Preferences} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; -import CustomTheme from '@screens/settings/display_theme/custom_theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import SettingContainer from '../setting_container'; + +import CustomTheme from './custom_theme'; import {ThemeTiles} from './theme_tiles'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - flex: 1, - paddingTop: 35, - }, - }; -}); - type DisplayThemeProps = { allowedThemeKeys: string[]; currentTeamId: string; @@ -37,8 +24,6 @@ const DisplayTheme = ({allowedThemeKeys, currentTeamId, currentUserId}: DisplayT const theme = useTheme(); const [customTheme, setCustomTheme] = useState(); - const styles = getStyleSheet(theme); - useEffect(() => { if (theme.type === 'custom') { setCustomTheme(theme); @@ -60,20 +45,18 @@ const DisplayTheme = ({allowedThemeKeys, currentTeamId, currentUserId}: DisplayT }, [serverUrl, allowedThemeKeys, currentTeamId]); return ( - - - + + {customTheme && ( + - {customTheme && ( - - )} - - + )} + ); }; diff --git a/app/screens/settings/display_theme/theme_tiles.tsx b/app/screens/settings/display_theme/theme_tiles.tsx index 33673a4b4..65a887146 100644 --- a/app/screens/settings/display_theme/theme_tiles.tsx +++ b/app/screens/settings/display_theme/theme_tiles.tsx @@ -71,7 +71,7 @@ export const ThemeTile = ({ theme, }: ThemeTileProps) => { const isTablet = useIsTablet(); - const style = getStyleSheet(activeTheme); + const styles = getStyleSheet(activeTheme); const {width: deviceWidth} = useWindowDimensions(); const layoutStyle = useMemo(() => { @@ -95,9 +95,9 @@ export const ThemeTile = ({ return ( - + )} diff --git a/app/screens/settings/display_timezone/display_timezone.tsx b/app/screens/settings/display_timezone/display_timezone.tsx index 318ce8a88..4cc24251b 100644 --- a/app/screens/settings/display_timezone/display_timezone.tsx +++ b/app/screens/settings/display_timezone/display_timezone.tsx @@ -4,10 +4,8 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; import {View} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; import {updateMe} from '@actions/remote/user'; -import OptionItem from '@components/option_item'; import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; @@ -15,38 +13,16 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; import {goToScreen, popTopScreen, setButtons} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {getDeviceTimezone} from '@utils/timezone'; import {getTimezoneRegion, getUserTimezoneProps} from '@utils/user'; +import {getSaveButton} from '../config'; +import SettingContainer from '../setting_container'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; + import type UserModel from '@typings/database/models/servers/user'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - flex: 1, - paddingTop: 35, - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - marginLeft: 15, - }, - content: { - paddingHorizontal: 8, - }, - }; -}); - -const edges: Edge[] = ['left', 'right']; const SAVE_TIMEZONE_BUTTON_ID = 'save_timezone'; type DisplayTimezoneProps = { @@ -59,8 +35,6 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { const timezone = useMemo(() => getUserTimezoneProps(currentUser), [currentUser.timezone]); const [userTimezone, setUserTimezone] = useState(timezone); const theme = useTheme(); - const styles = getStyleSheet(theme); - const updateAutomaticTimezone = (useAutomaticTimezone: boolean) => { const automaticTimezone = getDeviceTimezone(); setUserTimezone((prev) => ({ @@ -102,22 +76,18 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { close(); }, [userTimezone, currentUser.timezone, serverUrl]); - const saveButton = useMemo(() => { - return { - id: SAVE_TIMEZONE_BUTTON_ID, - enabled: false, - showAsAction: 'always' as const, - testID: 'notification_settings.auto_res.save.button', - color: theme.sidebarHeaderTextColor, - text: intl.formatMessage({id: 'settings.save', defaultMessage: 'Save'}), - }; - }, [theme.sidebarHeaderTextColor]); + const saveButton = useMemo(() => getSaveButton(SAVE_TIMEZONE_BUTTON_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]); useEffect(() => { + const enabled = + timezone.useAutomaticTimezone !== userTimezone.useAutomaticTimezone || + timezone.automaticTimezone !== userTimezone.automaticTimezone || + timezone.manualTimezone !== userTimezone.manualTimezone; + const buttons = { rightButtons: [{ ...saveButton, - enabled: timezone.useAutomaticTimezone !== userTimezone.useAutomaticTimezone, + enabled, }], }; setButtons(componentId, buttons); @@ -128,35 +98,28 @@ const DisplayTimezone = ({currentUser, componentId}: DisplayTimezoneProps) => { useAndroidHardwareBackHandler(componentId, close); return ( - - - - - {!userTimezone.useAutomaticTimezone && ( - - - - - )} - - - + + + + {!userTimezone.useAutomaticTimezone && ( + + + + + )} + + ); }; diff --git a/app/screens/settings/display_timezone_select/index.tsx b/app/screens/settings/display_timezone_select/index.tsx index d1e7c560c..7968b27ea 100644 --- a/app/screens/settings/display_timezone_select/index.tsx +++ b/app/screens/settings/display_timezone_select/index.tsx @@ -1,7 +1,7 @@ // 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 {FlatList, View} from 'react-native'; import {Edge, SafeAreaView} from 'react-native-safe-area-context'; @@ -31,6 +31,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { searchBar: { height: 38, marginVertical: 5, + marginBottom: 32, + }, + inputContainerStyle: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), + paddingHorizontal: 12, + marginLeft: 12, + marginTop: 12, }, }; }); @@ -57,6 +64,16 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); + const cancelButtonProps = useMemo(() => ({ + buttonTextStyle: { + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 100), + }, + buttonStyle: { + marginTop: 12, + }, + }), [theme.centerChannelColor]); + const [timezones, setTimezones] = useState(EMPTY_TIMEZONES); const [initialScrollIndex, setInitialScrollIndex] = useState(0); const [value, setValue] = useState(''); @@ -113,11 +130,12 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => { { return { itemContainer: { - flexDirection: 'row', + flexDirection: 'column', alignItems: 'center', - width: '100%', - paddingHorizontal: 15, + paddingHorizontal: 18, height: ITEM_HEIGHT, }, item: { @@ -27,7 +27,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }, itemText: { color: theme.centerChannelColor, - ...typography('Body', 100, 'Regular'), + ...typography('Body', 200, 'Regular'), + }, + body: { + flexDirection: 'row', + }, + lineStyles: { + width: '100%', }, }; }); @@ -40,28 +46,35 @@ const TimezoneRow = ({onPressTimezone, selectedTimezone, timezone}: TimezoneRowP const theme = useTheme(); const styles = getStyleSheet(theme); - const onTimezoneSelect = () => { + const onTimezoneSelect = useCallback(() => { onPressTimezone(timezone); - }; + }, [onPressTimezone, timezone]); return ( - - - {timezone} - + + + + {timezone} + + + {timezone === selectedTimezone && ( + + )} - {timezone === selectedTimezone && ( - - )} + ); }; 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 5ba8b2355..d99a9fb23 100644 --- a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx +++ b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx @@ -3,13 +3,10 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; import {updateMe} from '@actions/remote/user'; import FloatingTextInput from '@components/floating_text_input_label'; import FormattedText from '@components/formatted_text'; -import OptionItem from '@components/option_item'; import {General} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; @@ -21,11 +18,16 @@ import {changeOpacity, makeStyleSheetFromTheme, getKeyboardAppearanceFromTheme} import {typography} from '@utils/typography'; import {getNotificationProps} from '@utils/user'; +import {getSaveButton} from '../config'; +import SettingContainer from '../setting_container'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; + import type UserModel from '@typings/database/models/servers/user'; -const headerText = { - id: t('notification_settings.auto_responder'), - defaultMessage: 'Custom message', +const label = { + id: t('notification_settings.auto_responder.message'), + defaultMessage: 'Message', }; const OOO = { @@ -34,43 +36,25 @@ const OOO = { }; const SAVE_OOO_BUTTON_ID = 'notification_settings.auto_responder.save.button'; -const edges: Edge[] = ['left', 'right']; - const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - flex: 1, - paddingTop: 35, - }, input: { color: theme.centerChannelColor, - height: 150, - paddingHorizontal: 15, - paddingVertical: 10, ...typography('Body', 200, 'Regular'), + flex: 1, + }, + textInputContainer: { + width: '91%', + marginTop: 20, + alignSelf: 'center', + height: 154, }, footer: { - paddingHorizontal: 15, + paddingHorizontal: 20, color: changeOpacity(theme.centerChannelColor, 0.5), textAlign: 'justify', - ...typography('Body', 75), - }, - area: { - paddingHorizontal: 16, - }, - label: { - color: theme.centerChannelColor, - ...typography('Body', 100, 'Regular'), - }, - enabled: { - paddingHorizontal: 8, - backgroundColor: theme.centerChannelBg, - marginBottom: 16, + ...typography('Body', 75, 'Regular'), + marginTop: 20, }, }; }); @@ -83,53 +67,33 @@ const NotificationAutoResponder = ({currentUser, componentId}: NotificationAutoR const theme = useTheme(); const serverUrl = useServerUrl(); const intl = useIntl(); - const userNotifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser.notifyProps]); - const [autoResponderActive, setAutoResponderActive] = useState((currentUser.status === General.OUT_OF_OFFICE && userNotifyProps.auto_responder_active) ? 'true' : 'false'); - const [autoResponderMessage, setAutoResponderMessage] = useState(userNotifyProps.auto_responder_message || intl.formatMessage(OOO)); + const notifyProps = useMemo(() => getNotificationProps(currentUser), []); // dependency array should remain empty + + const initialAutoResponderActive = useMemo(() => Boolean(currentUser.status === General.OUT_OF_OFFICE && notifyProps.auto_responder_active === 'true'), []); // dependency array should remain empty + const [autoResponderActive, setAutoResponderActive] = useState(initialAutoResponderActive); + + const initialOOOMsg = useMemo(() => notifyProps.auto_responder_message || intl.formatMessage(OOO), []); // dependency array should remain empty + const [autoResponderMessage, setAutoResponderMessage] = useState(initialOOOMsg); const styles = getStyleSheet(theme); const close = () => popTopScreen(componentId); - const saveButton = useMemo(() => { - return { - id: SAVE_OOO_BUTTON_ID, - enabled: false, - showAsAction: 'always' as const, - testID: 'notification_settings.auto_res.save.button', - color: theme.sidebarHeaderTextColor, - text: intl.formatMessage({id: 'settings.save', defaultMessage: 'Save'}), - }; - }, [theme.sidebarHeaderTextColor]); - - const onAutoResponseToggle = useCallback((active: boolean) => { - setAutoResponderActive(`${active}`); - }, []); - - const onAutoResponseChangeText = useCallback((message: string) => { - setAutoResponderMessage(message); - }, []); + const saveButton = useMemo(() => getSaveButton(SAVE_OOO_BUTTON_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]); const saveAutoResponder = useCallback(() => { - const notifyProps = { - ...userNotifyProps, - auto_responder_active: autoResponderActive, - auto_responder_message: autoResponderMessage, - } as unknown as UserNotifyProps; - updateMe(serverUrl, { - notify_props: notifyProps, + notify_props: { + ...notifyProps, + auto_responder_active: `${autoResponderActive}`, + auto_responder_message: autoResponderMessage, + }, }); close(); - }, [serverUrl, autoResponderActive, autoResponderMessage, userNotifyProps]); + }, [serverUrl, autoResponderActive, autoResponderMessage, notifyProps]); useEffect(() => { - const updatedMsg = userNotifyProps?.auto_responder_message !== autoResponderMessage; - const enabling = currentUser.status !== General.OUT_OF_OFFICE && autoResponderActive === 'true'; - const disabling = currentUser.status === General.OUT_OF_OFFICE && autoResponderActive === 'false'; - - const enabled = enabling || disabling || updatedMsg; - + const enabled = initialAutoResponderActive !== autoResponderActive || initialOOOMsg !== autoResponderMessage; const buttons = { rightButtons: [{ ...saveButton, @@ -137,55 +101,48 @@ const NotificationAutoResponder = ({currentUser, componentId}: NotificationAutoR }], }; setButtons(componentId, buttons); - }, [autoResponderActive, autoResponderMessage, componentId, currentUser.status, userNotifyProps.auto_responder_message]); + }, [autoResponderActive, autoResponderMessage, componentId, currentUser.status, notifyProps.auto_responder_message]); useNavButtonPressed(SAVE_OOO_BUTTON_ID, componentId, saveAutoResponder, [saveAutoResponder]); useAndroidHardwareBackHandler(componentId, close); return ( - - - - - - {autoResponderActive === 'true' && ( - - )} - + + + {autoResponderActive && ( + - - + )} + + ); }; diff --git a/app/screens/settings/notification_email/index.tsx b/app/screens/settings/notification_email/index.tsx new file mode 100644 index 000000000..5ab545fb6 --- /dev/null +++ b/app/screens/settings/notification_email/index.tsx @@ -0,0 +1,33 @@ +// 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 {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; + +import {Preferences} from '@constants'; +import {getPreferenceValue} from '@helpers/api/preference'; +import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; +import {observeConfigBooleanValue} from '@queries/servers/system'; +import {observeIsCRTEnabled} from '@queries/servers/thread'; +import {observeCurrentUser} from '@queries/servers/user'; + +import NotificationEmail from './notification_email'; + +import type {WithDatabaseArgs} from '@typings/database/database'; + +const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { + return { + currentUser: observeCurrentUser(database), + enableEmailBatching: observeConfigBooleanValue(database, 'EnableEmailBatching'), + emailInterval: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_NOTIFICATIONS). + observeWithColumns(['value']).pipe( + switchMap((preferences) => of$(getPreferenceValue(preferences, Preferences.CATEGORY_NOTIFICATIONS, Preferences.EMAIL_INTERVAL, Preferences.INTERVAL_NOT_SET))), + ), + isCRTEnabled: observeIsCRTEnabled(database), + sendEmailNotifications: observeConfigBooleanValue(database, 'SendEmailNotifications'), + }; +}); + +export default withDatabase(enhanced(NotificationEmail)); diff --git a/app/screens/settings/notification_email/notification_email.tsx b/app/screens/settings/notification_email/notification_email.tsx new file mode 100644 index 000000000..a29a9ea9a --- /dev/null +++ b/app/screens/settings/notification_email/notification_email.tsx @@ -0,0 +1,203 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Text} from 'react-native'; + +import {savePreference} from '@actions/remote/preference'; +import {updateMe} from '@actions/remote/user'; +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 {t} from '@i18n'; +import {popTopScreen, setButtons} from '@screens/navigation'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; +import {getEmailInterval, getNotificationProps} from '@utils/user'; + +import {getSaveButton} from '../config'; +import SettingBlock from '../setting_block'; +import SettingContainer from '../setting_container'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; + +import type UserModel from '@typings/database/models/servers/user'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + disabled: { + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 75, 'Regular'), + }, + }; +}); + +const emailHeaderText = { + id: t('notification_settings.email.send'), + defaultMessage: 'Send email notifications', +}; +const emailFooterText = { + id: t('notification_settings.email.emailInfo'), + defaultMessage: 'Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.', +}; + +const emailHeaderCRTText = { + id: t('notification_settings.email.crt.send'), + defaultMessage: 'Thread reply notifications', +}; +const emailFooterCRTText = { + id: t('notification_settings.email.crt.emailInfo'), + defaultMessage: "When enabled, any reply to a thread you're following will send an email notification", +}; + +const SAVE_EMAIL_BUTTON_ID = 'settings_notification.email.save.button'; + +type NotificationEmailProps = { + componentId: string; + currentUser: UserModel; + emailInterval: string; + enableEmailBatching: boolean; + isCRTEnabled: boolean; + sendEmailNotifications: boolean; +} + +const NotificationEmail = ({componentId, currentUser, emailInterval, enableEmailBatching, isCRTEnabled, sendEmailNotifications}: NotificationEmailProps) => { + const notifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser.notifyProps]); + const initialInterval = useMemo(() => getEmailInterval( + sendEmailNotifications && notifyProps?.email === 'true', + enableEmailBatching, + parseInt(emailInterval, 10), + ).toString(), []); // dependency array should remain empty + const initialEmailThreads = useMemo(() => Boolean(notifyProps?.email_threads === 'all'), []); // dependency array should remain empty + + const [notifyInterval, setNotifyInterval] = useState(initialInterval); + const [emailThreads, setEmailThreads] = useState(initialEmailThreads); + + const intl = useIntl(); + const serverUrl = useServerUrl(); + const theme = useTheme(); + const styles = getStyleSheet(theme); + + const saveButton = useMemo(() => getSaveButton(SAVE_EMAIL_BUTTON_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]); + + const close = () => popTopScreen(componentId); + + const saveEmail = useCallback(async () => { + const promises = []; + const updatePromise = updateMe(serverUrl, { + notify_props: { + ...notifyProps, + email: `${sendEmailNotifications && notifyInterval !== Preferences.INTERVAL_NEVER.toString()}`, + email_threads: emailThreads ? 'all' : 'mention', + }, + }); + promises.push(updatePromise); + + if (notifyInterval !== initialInterval) { + const emailIntervalPreference = { + category: Preferences.CATEGORY_NOTIFICATIONS, + name: Preferences.EMAIL_INTERVAL, + user_id: currentUser.id, + value: notifyInterval, + }; + const savePrefPromise = savePreference(serverUrl, [emailIntervalPreference]); + promises.push(savePrefPromise); + } + await Promise.all(promises); + close(); + }, [notifyProps, notifyInterval, emailThreads, serverUrl, currentUser.id, sendEmailNotifications]); + + useEffect(() => { + const buttons = { + rightButtons: [{ + ...saveButton, + enabled: notifyInterval !== initialInterval || emailThreads !== initialEmailThreads, + }], + }; + setButtons(componentId, buttons); + }, [componentId, saveButton, notifyInterval, emailThreads]); + + useAndroidHardwareBackHandler(componentId, close); + useNavButtonPressed(SAVE_EMAIL_BUTTON_ID, componentId, saveEmail, [saveEmail]); + + return ( + + + {sendEmailNotifications && + <> + + + {enableEmailBatching && + <> + + + + + + } + + + } + {!sendEmailNotifications && + + {intl.formatMessage({ + id: 'notification_settings.email.emailHelp2', + defaultMessage: 'Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.', + })} + + } + + {isCRTEnabled && notifyProps.email === 'true' && ( + + + + + )} + + ); +}; + +export default NotificationEmail; diff --git a/app/screens/settings/notification_mention/mention_settings.tsx b/app/screens/settings/notification_mention/mention_settings.tsx index a822a9daf..2612d15dc 100644 --- a/app/screens/settings/notification_mention/mention_settings.tsx +++ b/app/screens/settings/notification_mention/mention_settings.tsx @@ -3,12 +3,10 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; +import {Text} from 'react-native'; import {updateMe} from '@actions/remote/user'; -import Block from '@components/block'; import FloatingTextInput from '@components/floating_text_input_label'; -import OptionItem from '@components/option_item'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; @@ -19,37 +17,22 @@ import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} import {typography} from '@utils/typography'; import {getNotificationProps} from '@utils/user'; +import {getSaveButton} from '../config'; +import SettingBlock from '../setting_block'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; + import type UserModel from '@typings/database/models/servers/user'; const mentionHeaderText = { - id: t('notification_settings.mentions.wordsTrigger'), - defaultMessage: 'Words that trigger mentions', + id: t('notification_settings.mentions.keywords_mention'), + defaultMessage: 'Keywords that trigger mentions', }; const SAVE_MENTION_BUTTON_ID = 'SAVE_MENTION_BUTTON_ID'; const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - flex: 1, - height: 1, - }, - upperCase: { - textTransform: 'uppercase', - }, - label: { - color: theme.centerChannelColor, - ...typography('Body', 200, 'Regular'), - }, - desc: { - color: theme.centerChannelColor, - ...typography('Body', 100, 'Regular'), - paddingLeft: 8, - }, - container: { - paddingHorizontal: 8, - }, input: { color: theme.centerChannelColor, height: 150, @@ -58,6 +41,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }, containerStyle: { marginTop: 30, + width: '90%', + alignSelf: 'center', + }, + keywordLabelStyle: { + marginLeft: 20, + marginTop: 4, + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 75, 'Regular'), }, }; }); @@ -97,16 +88,7 @@ const MentionSettings = ({componentId, currentUser}: MentionSectionProps) => { const styles = getStyleSheet(theme); const intl = useIntl(); - const saveButton = useMemo(() => { - return { - id: SAVE_MENTION_BUTTON_ID, - enabled: false, - showAsAction: 'always' as const, - testID: 'notification_settings.mentions.save.button', - color: theme.sidebarHeaderTextColor, - text: intl.formatMessage({id: 'settings.save', defaultMessage: 'Save'}), - }; - }, [theme.sidebarHeaderTextColor]); + const saveButton = useMemo(() => getSaveButton(SAVE_MENTION_BUTTON_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]); const close = () => popTopScreen(componentId); @@ -159,44 +141,40 @@ const MentionSettings = ({componentId, currentUser}: MentionSectionProps) => { useAndroidHardwareBackHandler(componentId, close); return ( - - { Boolean(currentUser?.firstName) && ( + {Boolean(currentUser?.firstName) && ( <> - - + ) } {Boolean(currentUser?.username) && ( - )} - - + - + { blurOnSubmit={true} containerStyle={styles.containerStyle} keyboardAppearance={getKeyboardAppearanceFromTheme(theme)} - label={intl.formatMessage({id: 'notification_settings.mentions.keywords', defaultMessage: 'Keywords'})} + label={intl.formatMessage({id: 'notification_settings.mentions.keywords', defaultMessage: 'Enter other keywords'})} multiline={true} onChangeText={onChangeText} placeholder={intl.formatMessage({id: 'notification_settings.mentions..keywordsDescription', defaultMessage: 'Other words that trigger a mention'})} @@ -216,7 +194,12 @@ const MentionSettings = ({componentId, currentUser}: MentionSectionProps) => { underlineColorAndroid='transparent' value={mentionKeys} /> - + + {intl.formatMessage({id: 'notification_settings.mentions.keywordsLabel', defaultMessage: 'Keywords are not case-sensitive. Separate keywords with commas.'})} + + ); }; diff --git a/app/screens/settings/notification_mention/notification_mention.tsx b/app/screens/settings/notification_mention/notification_mention.tsx index e22ca9e13..94191eae4 100644 --- a/app/screens/settings/notification_mention/notification_mention.tsx +++ b/app/screens/settings/notification_mention/notification_mention.tsx @@ -2,68 +2,28 @@ // See LICENSE.txt for license information. import React from 'react'; -import {ScrollView} from 'react-native'; -import {SafeAreaView} from 'react-native-safe-area-context'; -import {useTheme} from '@context/theme'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; +import SettingContainer from '../setting_container'; import MentionSettings from './mention_settings'; import ReplySettings from './reply_settings'; import type UserModel from '@typings/database/models/servers/user'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - input: { - color: theme.centerChannelColor, - height: 40, - ...typography('Body', 75, 'Regular'), - }, - scrollView: { - flex: 1, - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - }, - scrollViewContent: { - paddingVertical: 35, - }, - }; -}); - type NotificationMentionProps = { componentId: string; currentUser: UserModel; isCRTEnabled: boolean; } const NotificationMention = ({componentId, currentUser, isCRTEnabled}: NotificationMentionProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); - return ( - - - - {!isCRTEnabled && ( - - )} - - + + + {!isCRTEnabled && } + ); }; diff --git a/app/screens/settings/notification_mention/reply_settings.tsx b/app/screens/settings/notification_mention/reply_settings.tsx index 20970e5e4..fd826bc7a 100644 --- a/app/screens/settings/notification_mention/reply_settings.tsx +++ b/app/screens/settings/notification_mention/reply_settings.tsx @@ -3,82 +3,50 @@ import React, {useState} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; -import Block from '@components/block'; -import OptionItem from '@components/option_item'; -import {useTheme} from '@context/theme'; import {t} from '@i18n'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; + +import SettingBlock from '../setting_block'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; const replyHeaderText = { id: t('notification_settings.mention.reply'), defaultMessage: 'Send reply notifications for', }; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - flex: 1, - height: 1, - }, - upperCase: { - textTransform: 'uppercase', - }, - label: { - color: theme.centerChannelColor, - ...typography('Body', 200, 'Regular'), - }, - container: { - paddingHorizontal: 8, - }, - }; -}); - const ReplySettings = () => { - const [replyNotificationType, setReplyNotificationType] = useState('any'); //todo: initialize with value from db/api - const theme = useTheme(); - const styles = getStyleSheet(theme); + const [replyNotificationType, setReplyNotificationType] = useState('any'); const intl = useIntl(); - const setReplyNotifications = (notifType: string) => { - setReplyNotificationType(notifType); - }; - return ( - - - - + + - - + + - + ); }; diff --git a/app/screens/settings/notification_push/notification_push.tsx b/app/screens/settings/notification_push/notification_push.tsx index eb4661a08..dd337dec2 100644 --- a/app/screens/settings/notification_push/notification_push.tsx +++ b/app/screens/settings/notification_push/notification_push.tsx @@ -3,8 +3,6 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {useIntl} from 'react-intl'; -import {ScrollView} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; import {updateMe} from '@actions/remote/user'; import {useServerUrl} from '@context/server'; @@ -12,33 +10,17 @@ 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 {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {getNotificationProps} from '@utils/user'; +import {getSaveButton} from '../config'; +import SettingContainer from '../setting_container'; + import MobileSendPush from './push_send'; import MobilePushStatus from './push_status'; import MobilePushThread from './push_thread'; import type UserModel from '@typings/database/models/servers/user'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - scrollView: { - flex: 1, - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - }, - scrollViewContent: { - paddingVertical: 30, - }, - }; -}); - -const edges: Edge[] = ['left', 'right']; - const SAVE_NOTIF_BUTTON_ID = 'SAVE_NOTIF_BUTTON_ID'; type NotificationMobileProps = { @@ -58,22 +40,12 @@ const NotificationPush = ({componentId, currentUser, isCRTEnabled, sendPushNotif const intl = useIntl(); const theme = useTheme(); - const styles = getStyleSheet(theme); const onMobilePushThreadChanged = useCallback(() => { setPushThreadPref(pushThread === 'all' ? 'mention' : 'all'); }, [pushThread]); - const saveButton = useMemo(() => { - return { - id: SAVE_NOTIF_BUTTON_ID, - enabled: false, - showAsAction: 'always' as const, - testID: 'notification_settings.save.button', - color: theme.sidebarHeaderTextColor, - text: intl.formatMessage({id: 'settings.save', defaultMessage: 'Save'}), - }; - }, [theme.sidebarHeaderTextColor]); + const saveButton = useMemo(() => getSaveButton(SAVE_NOTIF_BUTTON_ID, intl, theme.sidebarHeaderTextColor), [theme.sidebarHeaderTextColor]); const close = useCallback(() => popTopScreen(componentId), [componentId]); @@ -110,35 +82,25 @@ const NotificationPush = ({componentId, currentUser, isCRTEnabled, sendPushNotif useAndroidHardwareBackHandler(componentId, close); return ( - - - + + {isCRTEnabled && pushSend === 'mention' && ( + - {isCRTEnabled && pushSend === 'mention' && ( - - )} - {sendPushNotifications && pushSend !== 'none' && ( - - )} - - + )} + {sendPushNotifications && pushSend !== 'none' && ( + + )} + ); }; diff --git a/app/screens/settings/notification_push/push_send.tsx b/app/screens/settings/notification_push/push_send.tsx index 9c95bd521..36eaf5cce 100644 --- a/app/screens/settings/notification_push/push_send.tsx +++ b/app/screens/settings/notification_push/push_send.tsx @@ -3,49 +3,35 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; -import Block from '@components/block'; import FormattedText from '@components/formatted_text'; -import OptionItem from '@components/option_item'; import {useTheme} from '@context/theme'; import {t} from '@i18n'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import SettingBlock from '../setting_block'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; + const headerText = { - id: t('notification_settings.send_notification'), - defaultMessage: 'Send notifications', + id: t('notification_settings.send_notification.about'), + defaultMessage: 'Notify me about...', }; const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { - upperCase: { - textTransform: 'uppercase', - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - flex: 1, - height: 1, - }, - label: { - color: theme.centerChannelColor, - ...typography('Body', 100, 'Regular'), - }, disabled: { color: theme.centerChannelColor, paddingHorizontal: 15, paddingVertical: 10, ...typography('Body', 200, 'Regular'), }, - container: { - paddingHorizontal: 8, - }, }; }); type MobileSendPushProps = { - sendPushNotifications: boolean; pushStatus: PushStatus; + sendPushNotifications: boolean; setMobilePushPref: (status: PushStatus) => void; } const MobileSendPush = ({sendPushNotifications, pushStatus, setMobilePushPref}: MobileSendPushProps) => { @@ -54,36 +40,32 @@ const MobileSendPush = ({sendPushNotifications, pushStatus, setMobilePushPref}: const intl = useIntl(); return ( - {sendPushNotifications && <> - - - + - - + } - + ); }; diff --git a/app/screens/settings/notification_push/push_status.tsx b/app/screens/settings/notification_push/push_status.tsx index 8e6fed3d8..0954494ec 100644 --- a/app/screens/settings/notification_push/push_status.tsx +++ b/app/screens/settings/notification_push/push_status.tsx @@ -3,82 +3,53 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; -import Block from '@components/block'; -import OptionItem from '@components/option_item'; -import {useTheme} from '@context/theme'; import {t} from '@i18n'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; + +import SettingBlock from '../setting_block'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; const headerText = { - id: t('notification_settings.mobile.push_status'), - defaultMessage: 'Trigger push notifications when', + id: t('notification_settings.mobile.trigger_push'), + defaultMessage: 'Trigger push notifications when...', }; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - upperCase: { - textTransform: 'uppercase', - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - flex: 1, - height: 1, - }, - label: { - color: theme.centerChannelColor, - ...typography('Body', 100, 'Regular'), - - }, - container: { - paddingHorizontal: 8, - }, - }; -}); - type MobilePushStatusProps = { pushStatus: PushStatus; setMobilePushStatus: (status: PushStatus) => void; } const MobilePushStatus = ({pushStatus, setMobilePushStatus}: MobilePushStatusProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); const intl = useIntl(); return ( - - - - + - - + - + ); }; diff --git a/app/screens/settings/notification_push/push_thread.tsx b/app/screens/settings/notification_push/push_thread.tsx index 31aab707f..70461967d 100644 --- a/app/screens/settings/notification_push/push_thread.tsx +++ b/app/screens/settings/notification_push/push_thread.tsx @@ -3,34 +3,18 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; +import {StyleSheet} from 'react-native'; -import Block from '@components/block'; -import OptionItem from '@components/option_item'; -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 { - upperCase: { - textTransform: 'uppercase', - }, - separator: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - flex: 1, - height: 1, - marginLeft: 15, - }, - area: { - paddingHorizontal: 16, - }, - label: { - color: theme.centerChannelColor, - ...typography('Body', 100, 'Regular'), - }, - }; +import SettingBlock from '../setting_block'; +import SettingOption from '../setting_option'; +import SettingSeparator from '../settings_separator'; + +const styles = StyleSheet.create({ + area: { + paddingHorizontal: 16, + }, }); const headerText = { @@ -48,25 +32,22 @@ type MobilePushThreadProps = { } const MobilePushThread = ({pushThread, onMobilePushThreadChanged}: MobilePushThreadProps) => { - const theme = useTheme(); const intl = useIntl(); - const styles = getStyleSheet(theme); return ( - - - - + + ); }; diff --git a/app/screens/settings/notifications/index.tsx b/app/screens/settings/notifications/index.tsx index 23f108992..cc68cc9ef 100644 --- a/app/screens/settings/notifications/index.tsx +++ b/app/screens/settings/notifications/index.tsx @@ -3,9 +3,15 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; +import {Preferences} from '@constants'; +import {getPreferenceValue} from '@helpers/api/preference'; +import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {observeConfigBooleanValue} from '@queries/servers/system'; import {observeIsCRTEnabled} from '@queries/servers/thread'; +import {observeCurrentUser} from '@queries/servers/user'; import NotificationSettings from './notifications'; @@ -13,8 +19,15 @@ import type {WithDatabaseArgs} from '@typings/database/database'; const enhanced = withObservables([], ({database}: WithDatabaseArgs) => { return { + currentUser: observeCurrentUser(database), isCRTEnabled: observeIsCRTEnabled(database), enableAutoResponder: observeConfigBooleanValue(database, 'ExperimentalEnableAutomaticReplies'), + enableEmailBatching: observeConfigBooleanValue(database, 'EnableEmailBatching'), + emailInterval: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_NOTIFICATIONS). + observeWithColumns(['value']).pipe( + switchMap((preferences) => of$(getPreferenceValue(preferences, Preferences.CATEGORY_NOTIFICATIONS, Preferences.EMAIL_INTERVAL, Preferences.INTERVAL_NOT_SET))), + ), + sendEmailNotifications: observeConfigBooleanValue(database, 'SendEmailNotifications'), }; }); diff --git a/app/screens/settings/notifications/notifications.tsx b/app/screens/settings/notifications/notifications.tsx index ba49bd785..e5e515530 100644 --- a/app/screens/settings/notifications/notifications.tsx +++ b/app/screens/settings/notifications/notifications.tsx @@ -1,41 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {Platform, ScrollView, View} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; -import {Screens} from '@constants'; -import {useTheme} from '@context/theme'; +import {General, Screens} from '@constants'; import {t} from '@i18n'; import {goToScreen} from '@screens/navigation'; -import SettingOption from '@screens/settings/setting_option'; -import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import SettingRowLabel from '@screens/settings/setting_row_label'; +import {getEmailInterval, getEmailIntervalTexts, getNotificationProps} from '@utils/user'; -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - ...Platform.select({ - ios: { - flex: 1, - paddingTop: 35, - }, - }), - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - width: '100%', - }, - }; -}); -const edges: Edge[] = ['left', 'right']; +import SettingContainer from '../setting_container'; +import SettingItem from '../setting_item'; + +import type UserModel from '@typings/database/models/servers/user'; const mentionTexts = { crtOn: { @@ -47,14 +25,33 @@ const mentionTexts = { defaultMessage: 'Mentions and Replies', }, }; + type NotificationsProps = { - isCRTEnabled: boolean; + currentUser: UserModel; + emailInterval: string; enableAutoResponder: boolean; + enableEmailBatching: boolean; + isCRTEnabled: boolean; + sendEmailNotifications: boolean; } -const Notifications = ({isCRTEnabled, enableAutoResponder}: NotificationsProps) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); +const Notifications = ({ + currentUser, + emailInterval, + enableAutoResponder, + enableEmailBatching, + isCRTEnabled, + sendEmailNotifications, +}: NotificationsProps) => { const intl = useIntl(); + const notifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser.notifyProps]); + + const emailIntervalPref = useMemo(() => + getEmailInterval( + sendEmailNotifications && notifyProps?.email === 'true', + enableEmailBatching, + parseInt(emailInterval, 10), + ).toString(), + [emailInterval, enableEmailBatching, notifyProps, sendEmailNotifications]); const goToNotificationSettingsMentions = useCallback(() => { const screen = Screens.SETTINGS_NOTIFICATION_MENTION; @@ -85,36 +82,45 @@ const Notifications = ({isCRTEnabled, enableAutoResponder}: NotificationsProps) goToScreen(screen, title); }, []); + const goToEmailSettings = useCallback(() => { + const screen = Screens.SETTINGS_NOTIFICATION_EMAIL; + const title = intl.formatMessage({id: 'notification_settings.email', defaultMessage: 'Email Notifications'}); + goToScreen(screen, title); + }, []); + return ( - - - - - - {enableAutoResponder && ( - + + + - )} - - - + } + /> + {enableAutoResponder && ( + + } + /> + )} + ); }; diff --git a/app/screens/settings/setting_block.tsx b/app/screens/settings/setting_block.tsx new file mode 100644 index 000000000..ba5e2c037 --- /dev/null +++ b/app/screens/settings/setting_block.tsx @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react'; + +import Block, {SectionText, BlockProps} from '@components/block'; +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + blockHeader: { + color: theme.centerChannelColor, + ...typography('Heading', 300, 'SemiBold'), + marginBottom: 16, + marginLeft: 18, + }, + }; +}); + +type SettingBlockProps = { + children: React.ReactNode; + headerText?: SectionText; +} & BlockProps; + +const SettingBlock = ({headerText, ...props}: SettingBlockProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + return ( + + + {props.children} + + ); +}; + +export default SettingBlock; diff --git a/app/screens/settings/setting_container.tsx b/app/screens/settings/setting_container.tsx new file mode 100644 index 000000000..a4af358d0 --- /dev/null +++ b/app/screens/settings/setting_container.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {ScrollView} from 'react-native'; +import {Edge, SafeAreaView} from 'react-native-safe-area-context'; + +import {useTheme} from '@context/theme'; +import {makeStyleSheetFromTheme} from '@utils/theme'; + +const edges: Edge[] = ['left', 'right']; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + flex: 1, + backgroundColor: theme.centerChannelBg, + }, + contentContainerStyle: { + marginTop: 20, + }, + }; +}); + +type SettingContainerProps = { + children: React.ReactNode; +} +const SettingContainer = ({children}: SettingContainerProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + return ( + + + {children} + + + ); +}; + +export default SettingContainer; diff --git a/app/screens/settings/setting_item.tsx b/app/screens/settings/setting_item.tsx new file mode 100644 index 000000000..075d24bd2 --- /dev/null +++ b/app/screens/settings/setting_item.tsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Platform} from 'react-native'; + +import MenuItem, {MenuItemProps} from '@components/menu_item'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import Options, {DisplayOptionConfig, NotificationsOptionConfig, SettingOptionConfig} from './config'; + +type SettingsConfig = keyof typeof SettingOptionConfig | keyof typeof NotificationsOptionConfig| keyof typeof DisplayOptionConfig +type SettingOptionProps = { + optionName: SettingsConfig; + onPress: () => void; +} & Omit; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + menuLabel: { + color: theme.centerChannelColor, + ...typography('Body', 200, 'Regular'), + }, + separatorStyle: { + width: '91%', + alignSelf: 'center', + }, + chevronStyle: { + marginRight: 14, + color: changeOpacity(theme.centerChannelColor, 0.32), + }, + }; +}); + +const SettingItem = ({onPress, optionName, ...rest}: SettingOptionProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + const props = {...rest, ...Options[optionName]} as unknown as Omit; + + return ( + + ); +}; + +export default SettingItem; diff --git a/app/screens/settings/setting_option.tsx b/app/screens/settings/setting_option.tsx index c033f965b..794204e15 100644 --- a/app/screens/settings/setting_option.tsx +++ b/app/screens/settings/setting_option.tsx @@ -2,42 +2,38 @@ // See LICENSE.txt for license information. import React from 'react'; -import {Platform} from 'react-native'; -import MenuItem, {MenuItemProps} from '@components/menu_item'; +import OptionItem, {OptionItemProps} from '@components/option_item'; import {useTheme} from '@context/theme'; -import {makeStyleSheetFromTheme} from '@utils/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; -import Options, {DisplayOptionConfig, NotificationsOptionConfig, SettingOptionConfig} from './constant'; - -type SettingsConfig = keyof typeof SettingOptionConfig | keyof typeof NotificationsOptionConfig| keyof typeof DisplayOptionConfig -type SettingOptionProps = { - optionName: SettingsConfig; - onPress: () => void; -} & Omit; - -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { +const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { - menuLabel: { + container: { + paddingHorizontal: 20, + }, + optionLabelTextStyle: { color: theme.centerChannelColor, - ...typography('Body', 200), + ...typography('Body', 200, 'Regular'), + marginBottom: 4, + }, + optionDescriptionTextStyle: { + color: changeOpacity(theme.centerChannelColor, 0.64), + ...typography('Body', 75, 'Regular'), }, }; }); -const SettingOption = ({onPress, optionName, ...rest}: SettingOptionProps) => { +const SettingOption = ({...props}: OptionItemProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); - const props = {...rest, ...Options[optionName]} as unknown as Omit; return ( - ); diff --git a/app/screens/settings/setting_row_label.tsx b/app/screens/settings/setting_row_label.tsx new file mode 100644 index 000000000..536ff13b8 --- /dev/null +++ b/app/screens/settings/setting_row_label.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Platform, Text} 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: { + color: changeOpacity(theme.centerChannelColor, 0.56), + ...typography('Body', 100, 'Regular'), + alignSelf: 'center', + ...Platform.select({ + android: { + marginRight: 20, + }, + }), + }, + }; +}); + +const SettingRowLabel = ({text}: SettingRowLabelProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + return ( + + {text} + + + ); +}; + +export default SettingRowLabel; diff --git a/app/screens/settings/settings.tsx b/app/screens/settings/settings.tsx index 749f2ba9c..ba66b4299 100644 --- a/app/screens/settings/settings.tsx +++ b/app/screens/settings/settings.tsx @@ -3,8 +3,7 @@ import React, {useEffect, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {Alert, Platform, ScrollView, View} from 'react-native'; -import {Edge, SafeAreaView} from 'react-native-safe-area-context'; +import {Alert, Platform, View} from 'react-native'; import CompassIcon from '@components/compass_icon'; import {Screens} from '@constants'; @@ -13,50 +12,27 @@ import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; import {dismissModal, goToScreen, setButtons} from '@screens/navigation'; +import SettingContainer from '@screens/settings/setting_container'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; -import {typography} from '@utils/typography'; import {tryOpenURL} from '@utils/url'; -import SettingOption from './setting_option'; +import SettingItem from './setting_item'; -const edges: Edge[] = ['left', 'right']; const CLOSE_BUTTON_ID = 'close-settings'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { return { - container: { - flex: 1, - backgroundColor: theme.centerChannelBg, - }, - wrapper: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), - ...Platform.select({ - ios: { - flex: 1, - paddingTop: 35, - }, - }), - }, - divider: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 1, - }, - middleDivider: { - borderTopWidth: 1, - borderBottomWidth: 1, - borderColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 35, - }, - group: { - backgroundColor: theme.centerChannelBg, - }, - innerContainerStyle: { + containerStyle: { paddingLeft: 8, + marginTop: 20, }, - menuLabel: { - color: theme.centerChannelColor, - ...typography('Body', 200), + helpGroup: { + width: '91%', + backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), + height: 1, + alignSelf: 'center', + marginTop: 20, }, }; }); @@ -68,14 +44,15 @@ type SettingsProps = { siteName: string; } -//todo: handle display on tablet and Profile the whole feature - https://mattermost.atlassian.net/browse/MM-39711 +//todo: Profile the whole feature - https://mattermost.atlassian.net/browse/MM-39711 const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) => { const theme = useTheme(); const intl = useIntl(); - const styles = getStyleSheet(theme); const serverDisplayName = useServerDisplayName(); + const serverName = siteName || serverDisplayName; + const styles = getStyleSheet(theme); const closeButton = useMemo(() => { return { @@ -142,59 +119,36 @@ const Settings = ({componentId, helpLink, showHelp, siteName}: SettingsProps) => } }); - let middleDividerStyle = styles.divider; - if (Platform.OS === 'ios') { - middleDividerStyle = styles.middleDivider; - } - return ( - - - - - - - - - - - - {showHelp && - - } - - - + + + + + + {Platform.OS === 'android' && } + {showHelp && + + } + ); }; diff --git a/app/screens/settings/settings_separator.tsx b/app/screens/settings/settings_separator.tsx new file mode 100644 index 000000000..3ed118c2c --- /dev/null +++ b/app/screens/settings/settings_separator.tsx @@ -0,0 +1,39 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Platform, StyleProp, View, ViewStyle} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + separator: { + ...Platform.select({ + ios: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), + width: '91%', + alignSelf: 'center', + height: 1, + marginTop: 12, + }, + default: { + display: 'none', + }, + }), + }, + }; +}); +type SettingSeparatorProps = { + lineStyles?: StyleProp; +} + +const SettingSeparator = ({lineStyles}: SettingSeparatorProps) => { + const theme = useTheme(); + const styles = getStyleSheet(theme); + + return (); +}; + +export default SettingSeparator; diff --git a/app/utils/user/index.ts b/app/utils/user/index.ts index 2ea0fbd50..e71e23983 100644 --- a/app/utils/user/index.ts +++ b/app/utils/user/index.ts @@ -329,7 +329,45 @@ export function getNotificationProps(user: UserModel) { push: 'mention', push_status: 'online', push_threads: 'all', + email_threads: 'all', }; return props; } + +export function getEmailInterval(enableEmailNotification: boolean, enableEmailBatching: boolean, emailIntervalPreference: number): number { + const { + INTERVAL_NEVER, + INTERVAL_IMMEDIATE, + INTERVAL_FIFTEEN_MINUTES, + INTERVAL_HOUR, + } = Preferences; + + const validValuesWithEmailBatching = [INTERVAL_IMMEDIATE, INTERVAL_NEVER, INTERVAL_FIFTEEN_MINUTES, INTERVAL_HOUR]; + const validValuesWithoutEmailBatching = [INTERVAL_IMMEDIATE, INTERVAL_NEVER]; + + if (!enableEmailNotification) { + return INTERVAL_NEVER; + } else if (enableEmailBatching && validValuesWithEmailBatching.indexOf(emailIntervalPreference) === -1) { + // When email batching is enabled, the default interval is 15 minutes + return INTERVAL_FIFTEEN_MINUTES; + } else if (!enableEmailBatching && validValuesWithoutEmailBatching.indexOf(emailIntervalPreference) === -1) { + // When email batching is not enabled, the default interval is immediately + return INTERVAL_IMMEDIATE; + } else if (enableEmailNotification && emailIntervalPreference === INTERVAL_NEVER) { + // When email notification is enabled, the default interval is immediately + return INTERVAL_IMMEDIATE; + } + + return emailIntervalPreference; +} + +export const getEmailIntervalTexts = (interval: string) => { + const intervalTexts: Record = { + [Preferences.INTERVAL_FIFTEEN_MINUTES]: {id: 'notification_settings.email.fifteenMinutes', defaultMessage: 'Every 15 minutes'}, + [Preferences.INTERVAL_HOUR]: {id: 'notification_settings.email.everyHour', defaultMessage: 'Every hour'}, + [Preferences.INTERVAL_IMMEDIATE]: {id: 'notification_settings.email.immediately', defaultMessage: 'Immediately'}, + [Preferences.INTERVAL_NEVER]: {id: 'notification_settings.email.never', defaultMessage: 'Never'}, + }; + return intervalTexts[interval]; +}; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index eb5e383a1..07f2c1b61 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -14,6 +14,7 @@ "account.settings": "Settings", "account.user_status.title": "User Presence", "account.your_profile": "Your Profile", + "advanced_settings.delete_data": "Delete Documents & Data", "alert.channel_deleted.description": "The channel {displayName} has been archived.", "alert.channel_deleted.title": "Archived channel", "alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", @@ -227,9 +228,13 @@ "custom_status.suggestions.working_from_home": "Working from home", "date_separator.today": "Today", "date_separator.yesterday": "Yesterday", + "display_settings.clock.military": "24-hour", + "display_settings.clock.standard": "12-hour", "display_settings.clockDisplay": "Clock Display", "display_settings.theme": "Theme", "display_settings.timezone": "Timezone", + "display_settings.tz.auto": "Auto", + "display_settings.tz.manual": "Manual", "download.error": "Unable to download the file. Try again later", "edit_post.editPost": "Edit the post...", "edit_post.save": "Save", @@ -398,8 +403,6 @@ "mobile.edit_post.delete_title": "Confirm Post Delete", "mobile.edit_post.error": "There was a problem editing this message. Please try again.", "mobile.edit_post.title": "Editing Message", - "mobile.emoji_picker.search.not_found_description": "Check the spelling or try another search.", - "mobile.emoji_picker.search.not_found_title": "No results found for \"{searchTerm}\"", "mobile.error_handler.button": "Relaunch", "mobile.error_handler.description": "\nTap relaunch to open the app again. After restart, you can report the problem from the settings menu.\n", "mobile.error_handler.title": "Unexpected error occurred", @@ -569,32 +572,44 @@ "msg_typing.isTyping": "{user} is typing...", "notification_settings.auto_responder": "Automatic Replies", "notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.", - "notification_settings.auto_responder.enabled": "Enabled", - "notification_settings.auto_responder.footer_message": "Set a custom message that will be automatically sent in response to Direct Messages. Mentions in Public and Private Channels will not trigger the automated reply. Enabling Automatic Replies sets your status to Out of Office and disables email and push notifications.", + "notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.", + "notification_settings.auto_responder.message": "", + "notification_settings.auto_responder.to.enable": "Enable automatic replies", + "notification_settings.email": "Email Notifications", + "notification_settings.email.crt.emailInfo": "When enabled, any reply to a thread you're following will send an email notification", + "notification_settings.email.crt.send": "Thread reply notifications", + "notification_settings.email.emailHelp2": "Email has been disabled by your System Administrator. No notification emails will be sent until it is enabled.", + "notification_settings.email.emailInfo": "Email notifications are sent for mentions and direct messages when you are offline or away for more than 5 minutes.", + "notification_settings.email.everyHour": "Every hour", + "notification_settings.email.fifteenMinutes": "Every 15 minutes", + "notification_settings.email.immediately": "Immediately", + "notification_settings.email.never": "Never", + "notification_settings.email.send": "Send email notifications", "notification_settings.mention.reply": "Send reply notifications for", "notification_settings.mentions": "Mentions", "notification_settings.mentions_replies": "Mentions and Replies", "notification_settings.mentions..keywordsDescription": "Other words that trigger a mention", "notification_settings.mentions.channelWide": "Channel-wide mentions", "notification_settings.mentions.keywords": "Keywords", + "notification_settings.mentions.keywords_mention": "", + "notification_settings.mentions.keywordsLabel": "Keywords are not case-sensitive. Separate keywords with commas.", "notification_settings.mentions.sensitiveName": "Your case sensitive first name", "notification_settings.mentions.sensitiveUsername": "Your non-case sensitive username", - "notification_settings.mentions.wordsTrigger": "Words that trigger mentions", "notification_settings.mobile": "Push Notifications", "notification_settings.mobile.away": "Away or offline", "notification_settings.mobile.offline": "Offline", "notification_settings.mobile.online": "Online, away or offline", - "notification_settings.mobile.push_status": "Trigger push notifications when", - "notification_settings.ooo_auto_responder": "Automatic Direct Message Replies", + "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.pushNotification.allActivity": "For all activity", + "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.never": "Never", - "notification_settings.pushNotification.onlyMentions": "Only for mentions and direct messages", - "notification_settings.send_notification": "Send notifications", + "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", "notification_settings.threads_start": "Threads that I start", "notification_settings.threads_start_participate": "Threads that I start or participate in", @@ -707,7 +722,6 @@ "settings.about": "About {appTitle}", "settings.advanced_settings": "Advanced Settings", "settings.display": "Display", - "settings.display.militaryClock.save": "Save", "settings.notifications": "Notifications", "settings.save": "Save", "snack.bar.favorited.channel": "This channel was favorited", @@ -766,6 +780,7 @@ "user.settings.general.nickname": "Nickname", "user.settings.general.position": "Position", "user.settings.general.username": "Username", + "user.settings.notifications.email_threads.description": "Notify me about all replies to threads I'm following", "user.tutorial.long_press": "Long-press on an item to view a user's profile", "video.download": "Download video", "video.failed_description": "An error occurred while trying to play the video.\n", diff --git a/types/api/users.d.ts b/types/api/users.d.ts index 05201f072..af276bcd7 100644 --- a/types/api/users.d.ts +++ b/types/api/users.d.ts @@ -17,6 +17,7 @@ type UserNotifyProps = { push_status: 'ooo' | 'offline' | 'away' | 'dnd' | 'online'; user_id?: string; push_threads?: 'all' | 'mention'; + email_threads?: 'all' | 'mention'; }; type UserProfile = {