MM-45221 - Gekidou Settings fixes - part 1 (#6472)

* modifying setting option

* navigates to email screen

* UI construction [in progress]

* hooking up withObservables

* email settings - need to save now

* adding a bit of paddings

* setting initial value

* Update notification_email.tsx

* UI Polish - main setting screen

* UI Polish - Mention

* UI Polish - Notification main screen

* code clean up

* code clean up

* UI Polish Notification

* UI Polish

* code clean up

* UI Polish - OOO

* fix observable for email interval

* fix ooo

* fix ooo

* added setting_row_label component

* further clean up

* UI Polish - Display - [ IN PROGRESS ]

* UI Polish - Display - [ IN PROGRESS ]

* UI Polish - Timezone Select [ IN PROGRESS ]

* Update index.test.tsx.snap

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

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

* refactor after review

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

Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
Avinash Lingaloo 2022-07-15 13:32:25 +04:00 committed by GitHub
parent 0f0c7d5795
commit 431406f09b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1220 additions and 1001 deletions

View file

@ -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]}
/>
}
<View style={[styles.items, containerStyles]}>
<View style={containerStyles}>
{children}
</View>
{(footerText && !disableFooter) &&

View file

@ -52,14 +52,13 @@ const BaseOption = ({
return (
<MenuItem
testID={testID}
labelComponent={label}
iconContainerStyle={styles.iconContainerStyle}
iconName={iconName}
isDestructor={isDestructive}
labelComponent={label}
onPress={onPress}
separator={false}
theme={theme}
isDestructor={isDestructive}
testID={testID}
/>
);
};

View file

@ -110,10 +110,13 @@ exports[`DrawerItem should match snapshot 1`] = `
</View>
<View
style={
Object {
"backgroundColor": "rgba(63,67,80,0.2)",
"height": 1,
}
Array [
Object {
"backgroundColor": "rgba(63,67,80,0.2)",
"height": 1,
},
undefined,
]
}
/>
</View>

View file

@ -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<ViewStyle>;
containerStyle?: StyleProp<ViewStyle>;
defaultMessage?: string;
i18nId?: string;
@ -78,24 +80,34 @@ export type MenuItemProps = {
onPress: () => void;
rightComponent?: ReactNode;
separator?: boolean;
separatorStyle?: StyleProp<ViewStyle>;
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 = (
<CompassIcon
name={iconName}
style={[style.icon, destructor]}
style={[style.icon, isDestructor && {color: theme.errorTextColor}]}
/>
);
}
@ -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) => {
<View style={style.labelContainer}>
{label}
</View>
{rightComponent}
{Boolean(showArrow) && (
<CompassIcon
name='chevron-right'
style={style.chevron}
style={[style.chevron, chevronStyle]}
/>
)}
{rightComponent}
</View>
{Boolean(separator) && (<View style={style.divider}/>)}
{Boolean(separator) && (<View style={[style.divider, separatorStyle]}/>)}
</View>
</TouchableWithFeedback>
);

View file

@ -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<React.SetStateAction<string | boolean>>)|((value: string | boolean) => void) ;
description?: string;
inline?: boolean;
destructive?: boolean;
@ -22,6 +22,8 @@ type Props = {
type: OptionType;
value?: string;
containerStyle?: StyleProp<ViewStyle>;
optionLabelTextStyle?: StyleProp<TextStyle>;
optionDescriptionTextStyle?: StyleProp<TextStyle>;
}
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 = ({
)}
<View style={labelStyle}>
<Text
style={labelTextStyle}
style={[optionLabelTextStyle, labelTextStyle]}
testID={`${testID}.label`}
>
{label}
</Text>
{Boolean(description) &&
<Text
style={descriptionTextStyle}
style={[optionDescriptionTextStyle, descriptionTextStyle]}
testID={`${testID}.description`}
>
{description}
@ -199,10 +212,11 @@ const OptionItem = ({
</View>
{Boolean(actionComponent || info) &&
<View style={styles.actionContainer}>
{Boolean(info) &&
<View style={styles.infoContainer}>
<Text style={styles.info}>{info}</Text>
</View>
{
Boolean(info) &&
<View style={styles.infoContainer}>
<Text style={[styles.info, destructive && {color: theme.dndIndicator}]}>{info}</Text>
</View>
}
{actionComponent}
</View>

View file

@ -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,

View file

@ -88,7 +88,6 @@ const CustomStatus = ({isCustomStatusExpirySupported, isTablet, currentUser}: Cu
/>}
separator={false}
onPress={goToCustomStatusScreen}
theme={theme}
/>
);
};

View file

@ -89,7 +89,6 @@ const AccountOptions = ({user, enableCustomUserStatuses, isCustomStatusExpirySup
<Settings
isTablet={isTablet}
style={styles.menuLabel}
theme={theme}
/>
<View style={styles.divider}/>
<Logout

View file

@ -50,7 +50,8 @@ const Settings = ({style, theme}: Props) => {
return (
<MenuItem
testID='account.logout.action'
iconName='exit-to-app'
isDestructor={true}
labelComponent={(
<View>
<FormattedText
@ -66,11 +67,9 @@ const Settings = ({style, theme}: Props) => {
/>
</View>
)}
iconName='exit-to-app'
isDestructor={true}
onPress={onLogout}
separator={false}
theme={theme}
testID='account.logout.action'
/>
);
};

View file

@ -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 (
<MenuItem
testID='account.settings.action'
iconName='settings-outline'
labelComponent={
<FormattedText
id='account.settings'
@ -42,10 +41,9 @@ const Settings = ({isTablet, style, theme}: Props) => {
style={style}
/>
}
iconName='settings-outline'
onPress={openSettings}
separator={false}
theme={theme}
testID='account.settings.action'
/>
);
};

View file

@ -120,7 +120,6 @@ const UserStatus = ({currentUser, style, theme}: Props) => {
return (
<MenuItem
testID='account.status.action'
labelComponent={
<StatusLabel
labelStyle={style}
@ -133,9 +132,9 @@ const UserStatus = ({currentUser, style, theme}: Props) => {
status={currentUser.status}
/>
}
separator={false}
onPress={handleSetStatus}
theme={theme}
separator={false}
testID='account.status.action'
/>
);
};

View file

@ -33,7 +33,7 @@ const YourProfile = ({isTablet, style, theme}: Props) => {
return (
<MenuItem
testID='account.your_profile.action'
iconName={ACCOUNT_OUTLINE_IMAGE}
labelComponent={
<FormattedText
id='account.your_profile'
@ -41,10 +41,9 @@ const YourProfile = ({isTablet, style, theme}: Props) => {
style={style}
/>
}
iconName={ACCOUNT_OUTLINE_IMAGE}
onPress={openProfile}
separator={false}
theme={theme}
testID='account.your_profile.action'
/>
);
};

View file

@ -37,8 +37,6 @@ const ShowMoreButton = ({onPress, showMore}: ShowMoreButtonProps) => {
return (
<MenuItem
testID={'mobile.search.show_more'}
onPress={onPress}
labelComponent={
<FormattedText
id={id}
@ -46,8 +44,9 @@ const ShowMoreButton = ({onPress, showMore}: ShowMoreButtonProps) => {
style={style.showMore}
/>
}
onPress={onPress}
separator={false}
theme={theme}
testID={'mobile.search.show_more'}
/>
);
};

View file

@ -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;

View file

@ -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 (
<SafeAreaView
edges={EDGES}
style={styles.screen}
testID='settings_display.screen'
>
<View
style={styles.body}
<SettingContainer>
<TouchableOpacity
onPress={onPressDeleteData}
disabled={disabled}
activeOpacity={disabled ? 0 : 1}
>
<TouchableOpacity
onPress={onPressDeleteData}
disabled={disabled}
activeOpacity={disabled ? 0 : 1}
>
<OptionItem
containerStyle={styles.itemStyle}
destructive={true}
label={intl.formatMessage({id: 'advanced_settings.delete_data', defaultMessage: 'Delete Documents & Data'})}
info={getFormattedFileSize(dataSize || 0)}
type='none'
/>
</TouchableOpacity>
</View>
</SafeAreaView>
<SettingOption
containerStyle={styles.itemStyle}
destructive={true}
icon='trash-can-outline'
info={getFormattedFileSize(dataSize || 0)}
label={intl.formatMessage({id: 'advanced_settings.delete_data', defaultMessage: 'Delete Documents & Data'})}
type='none'
/>
</TouchableOpacity>
</SettingContainer>
);
};

View file

@ -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',

View file

@ -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 (
<SafeAreaView
edges={['left', 'right']}
testID='notification_display.screen'
style={styles.container}
>
<ScrollView
contentContainerStyle={styles.wrapper}
alwaysBounceVertical={false}
>
<View style={styles.divider}/>
{isThemeSwitchingEnabled && (
<SettingOption
optionName='theme'
onPress={goToThemeSettings}
/>
)}
<SettingOption
optionName='clock'
onPress={goToClockDisplaySettings}
<SettingContainer>
{isThemeSwitchingEnabled && (
<SettingItem
optionName='theme'
onPress={goToThemeSettings}
rightComponent={
<SettingRowLabel
text={theme.type || ''}
/>
}
/>
{isTimezoneEnabled && (
<SettingOption
optionName='timezone'
onPress={goToTimezoneSettings}
)}
<SettingItem
optionName='clock'
onPress={goToClockDisplaySettings}
rightComponent={
<SettingRowLabel
text={intl.formatMessage(hasMilitaryTimeFormat ? TIME_FORMAT[1] : TIME_FORMAT[0])}
/>
)}
<View style={styles.divider}/>
</ScrollView>
</SafeAreaView>
}
/>
{isTimezoneEnabled && (
<SettingItem
optionName='timezone'
onPress={goToTimezoneSettings}
rightComponent={
<SettingRowLabel
text={intl.formatMessage(timezone.useAutomaticTimezone ? TIMEZONE_FORMAT[0] : TIMEZONE_FORMAT[1])}
/>
}
/>
)}
</SettingContainer>
);
};

View file

@ -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),
};
});

View file

@ -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 (
<SafeAreaView
edges={edges}
style={styles.container}
testID='settings_display.screen'
>
<View style={styles.wrapper}>
<Block
disableHeader={true}
footerText={footer}
>
<OptionItem
action={onSelectClockPreference}
containerStyle={styles.containerStyle}
label={intl.formatMessage({id: 'settings_display.clock.normal', defaultMessage: '12-hour clock (example: 4:00 PM)'})}
selected={!isMilitaryTimeFormat}
testID='clock_display_settings.normal_clock.action'
type='select'
value={CLOCK_TYPE.NORMAL}
/>
<View style={styles.divider}/>
<OptionItem
action={onSelectClockPreference}
containerStyle={styles.containerStyle}
label={intl.formatMessage({id: 'settings_display.clock.military', defaultMessage: '24-hour clock (example: 16:00)'})}
selected={isMilitaryTimeFormat}
testID='clock_display_settings.military_clock.action'
type='select'
value={CLOCK_TYPE.MILITARY}
/>
</Block>
</View>
</SafeAreaView>
<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)'})}
selected={!isMilitaryTimeFormat}
testID='clock_display_settings.normal_clock.action'
type='select'
value={CLOCK_TYPE.NORMAL}
/>
<SettingSeparator/>
<SettingOption
action={onSelectClockPreference}
label={intl.formatMessage({id: 'settings_display.clock.military', defaultMessage: '24-hour clock (example: 16:00)'})}
selected={isMilitaryTimeFormat}
testID='clock_display_settings.military_clock.action'
type='select'
value={CLOCK_TYPE.MILITARY}
/>
</SettingBlock>
</SettingContainer>
);
};

View file

@ -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 (
<Block
<SettingBlock
containerStyles={styles.containerStyles}
disableHeader={true}
>
<OptionItem
<SettingOption
action={setTheme}
type='select'
value={customTheme.type}
label={intl.formatMessage({id: 'settings_display.custom_theme', defaultMessage: 'Custom Theme'})}
selected={theme.type?.toLowerCase() === customTheme.type?.toLowerCase()}
/>
</Block>
</SettingBlock>
);
};

View file

@ -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<Theme|null>();
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 (
<ScrollView style={styles.container}>
<View style={styles.wrapper}>
<ThemeTiles
allowedThemeKeys={allowedThemeKeys}
onThemeChange={updateTheme}
<SettingContainer>
<ThemeTiles
allowedThemeKeys={allowedThemeKeys}
onThemeChange={updateTheme}
/>
{customTheme && (
<CustomTheme
customTheme={customTheme}
setTheme={updateTheme}
/>
{customTheme && (
<CustomTheme
customTheme={customTheme}
setTheme={updateTheme}
/>
)}
</View>
</ScrollView>
)}
</SettingContainer>
);
};

View file

@ -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 (
<TouchableOpacity
onPress={onPressHandler}
style={[style.container, layoutStyle.container]}
style={[styles.container, layoutStyle.container]}
>
<View style={[style.imageWrapper, layoutStyle.thumbnail]}>
<View style={[styles.imageWrapper, layoutStyle.thumbnail]}>
<ThemeThumbnail
borderColorBase={selected ? activeTheme.sidebarTextActiveBorder : activeTheme.centerChannelBg}
borderColorMix={selected ? activeTheme.sidebarTextActiveBorder : changeOpacity(activeTheme.centerChannelColor, 0.16)}
@ -108,7 +108,7 @@ export const ThemeTile = ({
<CompassIcon
name='check-circle'
size={31.2}
style={style.check}
style={styles.check}
/>
)}
</View>

View file

@ -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 (
<SafeAreaView
edges={edges}
style={styles.container}
>
<View style={styles.wrapper}>
<View style={styles.divider}/>
<OptionItem
action={updateAutomaticTimezone}
containerStyle={styles.content}
description={getTimezoneRegion(userTimezone.automaticTimezone)}
label={intl.formatMessage({id: 'settings_display.timezone.automatically', defaultMessage: 'Set automatically'})}
selected={userTimezone.useAutomaticTimezone}
type='toggle'
/>
{!userTimezone.useAutomaticTimezone && (
<View>
<View style={styles.separator}/>
<OptionItem
action={goToSelectTimezone}
containerStyle={styles.content}
description={getTimezoneRegion(userTimezone.manualTimezone)}
label={intl.formatMessage({id: 'settings_display.timezone.manual', defaultMessage: 'Change timezone'})}
type='arrow'
/>
</View>
)}
<View style={styles.divider}/>
</View>
</SafeAreaView>
<SettingContainer>
<SettingSeparator/>
<SettingOption
action={updateAutomaticTimezone}
description={getTimezoneRegion(userTimezone.automaticTimezone)}
label={intl.formatMessage({id: 'settings_display.timezone.automatically', defaultMessage: 'Set automatically'})}
selected={userTimezone.useAutomaticTimezone}
type='toggle'
/>
{!userTimezone.useAutomaticTimezone && (
<View>
<SettingSeparator/>
<SettingOption
action={goToSelectTimezone}
description={getTimezoneRegion(userTimezone.manualTimezone)}
label={intl.formatMessage({id: 'settings_display.timezone.manual', defaultMessage: 'Change timezone'})}
type='arrow'
/>
</View>
)}
<SettingSeparator/>
</SettingContainer>
);
};

View file

@ -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<string[]>(EMPTY_TIMEZONES);
const [initialScrollIndex, setInitialScrollIndex] = useState<number>(0);
const [value, setValue] = useState('');
@ -113,11 +130,12 @@ const SelectTimezones = ({selectedTimezone, onBack}: SelectTimezonesProps) => {
<View style={styles.searchBar}>
<Search
autoCapitalize='none'
containerStyle={styles.searchBarContainer}
cancelButtonProps={cancelButtonProps}
inputContainerStyle={styles.inputContainerStyle}
inputStyle={styles.searchBarInput}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onChangeText={setValue}
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
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'

View file

@ -1,11 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import SettingSeparator from '@screens/settings/settings_separator';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -14,10 +15,9 @@ const ITEM_HEIGHT = 45;
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
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 (
<TouchableOpacity
style={styles.itemContainer}
key={timezone}
onPress={onTimezoneSelect}
style={styles.itemContainer}
>
<View style={styles.item}>
<Text style={styles.itemText}>
{timezone}
</Text>
<View
style={styles.body}
>
<View style={styles.item}>
<Text style={styles.itemText}>
{timezone}
</Text>
</View>
{timezone === selectedTimezone && (
<CompassIcon
color={theme.linkColor}
name='check'
size={24}
/>
)}
</View>
{timezone === selectedTimezone && (
<CompassIcon
name='check'
size={24}
color={theme.linkColor}
/>
)}
<SettingSeparator
lineStyles={styles.lineStyles}
/>
</TouchableOpacity>
);
};

View file

@ -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<boolean>(initialAutoResponderActive);
const initialOOOMsg = useMemo(() => notifyProps.auto_responder_message || intl.formatMessage(OOO), []); // dependency array should remain empty
const [autoResponderMessage, setAutoResponderMessage] = useState<string>(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 (
<SafeAreaView
edges={edges}
style={styles.container}
>
<View style={styles.wrapper}>
<View
style={styles.enabled}
>
<OptionItem
label={intl.formatMessage({id: 'notification_settings.auto_responder.enabled', defaultMessage: 'Enabled'})}
action={onAutoResponseToggle}
type='toggle'
selected={autoResponderActive === 'true'}
/>
</View>
{autoResponderActive === 'true' && (
<FloatingTextInput
allowFontScaling={true}
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
label={intl.formatMessage(headerText)}
multiline={true}
onChangeText={onAutoResponseChangeText}
placeholder={intl.formatMessage(headerText)}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
returnKeyType='done'
textInputStyle={styles.input}
textAlignVertical='top'
theme={theme}
underlineColorAndroid='transparent'
value={autoResponderMessage || ''}
/>
)}
<FormattedText
id={'notification_settings.auto_responder.footer_message'}
defaultMessage={'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.'}
style={styles.footer}
<SettingContainer>
<SettingOption
label={intl.formatMessage({id: 'notification_settings.auto_responder.to.enable', defaultMessage: 'Enable automatic replies'})}
action={setAutoResponderActive}
type='toggle'
selected={autoResponderActive}
/>
<SettingSeparator/>
{autoResponderActive && (
<FloatingTextInput
allowFontScaling={true}
autoCapitalize='none'
autoCorrect={false}
blurOnSubmit={true}
containerStyle={styles.textInputContainer}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
label={intl.formatMessage(label)}
multiline={true}
onChangeText={setAutoResponderMessage}
placeholder={intl.formatMessage(label)}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
returnKeyType='done'
textAlignVertical='top'
textInputStyle={styles.input}
theme={theme}
underlineColorAndroid='transparent'
value={autoResponderMessage || ''}
/>
</View>
</SafeAreaView>
)}
<FormattedText
id={'notification_settings.auto_responder.footer.message'}
defaultMessage={'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.'}
style={styles.footer}
/>
</SettingContainer>
);
};

View file

@ -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));

View file

@ -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<string>(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 (
<SettingContainer>
<SettingBlock
disableFooter={!sendEmailNotifications}
footerText={emailFooterText}
headerText={emailHeaderText}
>
{sendEmailNotifications &&
<>
<SettingOption
action={setNotifyInterval}
label={intl.formatMessage({id: 'notification_settings.email.immediately', defaultMessage: 'Immediately'})}
selected={notifyInterval === `${Preferences.INTERVAL_IMMEDIATE}`}
testID='notification_settings.email.immediately.action'
type='select'
value={`${Preferences.INTERVAL_IMMEDIATE}`}
/>
<SettingSeparator/>
{enableEmailBatching &&
<>
<SettingOption
action={setNotifyInterval}
label={intl.formatMessage({id: 'notification_settings.email.fifteenMinutes', defaultMessage: 'Every 15 minutes'})}
selected={notifyInterval === `${Preferences.INTERVAL_FIFTEEN_MINUTES}`}
type='select'
value={`${Preferences.INTERVAL_FIFTEEN_MINUTES}`}
/>
<SettingSeparator/>
<SettingOption
action={setNotifyInterval}
label={intl.formatMessage({id: 'notification_settings.email.everyHour', defaultMessage: 'Every hour'})}
selected={notifyInterval === `${Preferences.INTERVAL_HOUR}`}
type='select'
value={`${Preferences.INTERVAL_HOUR}`}
/>
<SettingSeparator/>
</>
}
<SettingOption
action={setNotifyInterval}
label={intl.formatMessage({id: 'notification_settings.email.never', defaultMessage: 'Never'})}
selected={notifyInterval === `${Preferences.INTERVAL_NEVER}`}
testID='notification_settings.email.never.action'
type='select'
value={`${Preferences.INTERVAL_NEVER}`}
/>
</>
}
{!sendEmailNotifications &&
<Text
style={styles.disabled}
>
{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.',
})}
</Text>
}
</SettingBlock>
{isCRTEnabled && notifyProps.email === 'true' && (
<SettingBlock
footerText={emailFooterCRTText}
headerText={emailHeaderCRTText}
>
<SettingOption
action={setEmailThreads}
label={intl.formatMessage({id: 'user.settings.notifications.email_threads.description', defaultMessage: 'Notify me about all replies to threads I\'m following'})}
selected={emailThreads}
type='toggle'
/>
<SettingSeparator/>
</SettingBlock>
)}
</SettingContainer>
);
};
export default NotificationEmail;

View file

@ -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 (
<Block
<SettingBlock
headerText={mentionHeaderText}
headerStyles={styles.upperCase}
>
{ Boolean(currentUser?.firstName) && (
{Boolean(currentUser?.firstName) && (
<>
<OptionItem
<SettingOption
action={onToggleFirstName}
containerStyle={styles.container}
description={intl.formatMessage({id: 'notification_settings.mentions.sensitiveName', defaultMessage: 'Your case sensitive first name'})}
label={currentUser.firstName}
selected={tglFirstName}
type='toggle'
/>
<View style={styles.separator}/>
<SettingSeparator/>
</>
)
}
{Boolean(currentUser?.username) && (
<OptionItem
<SettingOption
action={onToggleUserName}
containerStyle={styles.container}
description={intl.formatMessage({id: 'notification_settings.mentions.sensitiveUsername', defaultMessage: 'Your non-case sensitive username'})}
label={currentUser.username}
selected={tglUserName}
type='toggle'
/>
)}
<View style={styles.separator}/>
<OptionItem
<SettingSeparator/>
<SettingOption
action={onToggleChannel}
containerStyle={styles.container}
description={intl.formatMessage({id: 'notification_settings.mentions.channelWide', defaultMessage: 'Channel-wide mentions'})}
label='@channel, @all, @here'
selected={tglChannel}
type='toggle'
/>
<View style={styles.separator}/>
<SettingSeparator/>
<FloatingTextInput
allowFontScaling={true}
autoCapitalize='none'
@ -204,7 +182,7 @@ const MentionSettings = ({componentId, currentUser}: MentionSectionProps) => {
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}
/>
</Block>
<Text
style={styles.keywordLabelStyle}
>
{intl.formatMessage({id: 'notification_settings.mentions.keywordsLabel', defaultMessage: 'Keywords are not case-sensitive. Separate keywords with commas.'})}
</Text>
</SettingBlock>
);
};

View file

@ -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 (
<SafeAreaView
edges={['left', 'right']}
testID='notification_mention.screen'
style={styles.container}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContent}
alwaysBounceVertical={false}
>
<MentionSettings
currentUser={currentUser}
componentId={componentId}
/>
{!isCRTEnabled && (
<ReplySettings/>
)}
</ScrollView>
</SafeAreaView>
<SettingContainer>
<MentionSettings
currentUser={currentUser}
componentId={componentId}
/>
{!isCRTEnabled && <ReplySettings/>}
</SettingContainer>
);
};

View file

@ -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 (
<Block
<SettingBlock
headerText={replyHeaderText}
headerStyles={styles.upperCase}
>
<OptionItem
action={setReplyNotifications}
type='select'
value='any'
containerStyle={styles.container}
<SettingOption
action={setReplyNotificationType}
label={intl.formatMessage({id: 'notification_settings.threads_start_participate', defaultMessage: 'Threads that I start or participate in'})}
selected={replyNotificationType === 'any'}
/>
<View style={styles.separator}/>
<OptionItem
action={setReplyNotifications}
type='select'
value='root'
containerStyle={styles.container}
value='any'
/>
<SettingSeparator/>
<SettingOption
action={setReplyNotificationType}
label={intl.formatMessage({id: 'notification_settings.threads_start', defaultMessage: 'Threads that I start'})}
selected={replyNotificationType === 'root'}
/>
<View style={styles.separator}/>
<OptionItem
action={setReplyNotifications}
type='select'
value='never'
containerStyle={styles.container}
value='root'
/>
<SettingSeparator/>
<SettingOption
action={setReplyNotificationType}
label={intl.formatMessage({id: 'notification_settings.threads_mentions', defaultMessage: 'Mentions in threads'})}
selected={replyNotificationType === 'never'}
type='select'
value='never'
/>
</Block>
</SettingBlock>
);
};

View file

@ -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 (
<SafeAreaView
edges={edges}
testID='notification_push.screen'
style={styles.container}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContent}
alwaysBounceVertical={false}
>
<MobileSendPush
pushStatus={pushSend}
sendPushNotifications={sendPushNotifications}
setMobilePushPref={setPushSend}
<SettingContainer>
<MobileSendPush
pushStatus={pushSend}
sendPushNotifications={sendPushNotifications}
setMobilePushPref={setPushSend}
/>
{isCRTEnabled && pushSend === 'mention' && (
<MobilePushThread
pushThread={pushThread}
onMobilePushThreadChanged={onMobilePushThreadChanged}
/>
{isCRTEnabled && pushSend === 'mention' && (
<MobilePushThread
pushThread={pushThread}
onMobilePushThreadChanged={onMobilePushThreadChanged}
/>
)}
{sendPushNotifications && pushSend !== 'none' && (
<MobilePushStatus
pushStatus={pushStatus}
setMobilePushStatus={setPushStatus}
/>
)}
</ScrollView>
</SafeAreaView>
)}
{sendPushNotifications && pushSend !== 'none' && (
<MobilePushStatus
pushStatus={pushStatus}
setMobilePushStatus={setPushStatus}
/>
)}
</SettingContainer>
);
};

View file

@ -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 (
<Block
<SettingBlock
headerText={headerText}
headerStyles={styles.upperCase}
>
{sendPushNotifications &&
<>
<OptionItem
<SettingOption
action={setMobilePushPref}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.pushNotification.allActivity', defaultMessage: 'For all activity'})}
label={intl.formatMessage({id: 'notification_settings.pushNotification.all_new_messages', defaultMessage: 'All new messages'})}
selected={pushStatus === 'all'}
testID='notification_settings.pushNotification.allActivity'
type='select'
value='all'
/>
<View style={styles.separator}/>
<OptionItem
<SettingSeparator/>
<SettingOption
action={setMobilePushPref}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.pushNotification.onlyMentions', defaultMessage: 'Only for mentions and direct messages'})}
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'
value='mention'
/>
<View style={styles.separator}/>
<OptionItem
<SettingSeparator/>
<SettingOption
action={setMobilePushPref}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.pushNotification.never', defaultMessage: 'Never'})}
label={intl.formatMessage({id: 'notification_settings.pushNotification.nothing', defaultMessage: 'Nothing'})}
selected={pushStatus === 'none'}
testID='notification_settings.pushNotification.never'
type='select'
@ -98,7 +80,7 @@ const MobileSendPush = ({sendPushNotifications, pushStatus, setMobilePushPref}:
style={styles.disabled}
/>
}
</Block>
</SettingBlock>
);
};

View file

@ -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 (
<Block
<SettingBlock
headerText={headerText}
headerStyles={styles.upperCase}
>
<OptionItem
<SettingOption
action={setMobilePushStatus}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.mobile.online', defaultMessage: 'Online, away or offline'})}
selected={pushStatus === 'online'}
type='select'
value='online'
/>
<View style={styles.separator}/>
<OptionItem
<SettingSeparator/>
<SettingOption
action={setMobilePushStatus}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.mobile.away', defaultMessage: 'Away or offline'})}
selected={pushStatus === 'away'}
type='select'
value='away'
/>
<View style={styles.separator}/>
<OptionItem
<SettingSeparator/>
<SettingOption
action={setMobilePushStatus}
containerStyle={styles.container}
label={intl.formatMessage({id: 'notification_settings.mobile.offline', defaultMessage: 'Offline'})}
selected={pushStatus === 'offline'}
type='select'
value='offline'
/>
</Block>
</SettingBlock>
);
};

View file

@ -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 (
<Block
<SettingBlock
headerText={headerText}
footerText={footerText}
headerStyles={styles.upperCase}
containerStyles={styles.area}
>
<OptionItem
<SettingOption
action={onMobilePushThreadChanged}
label={intl.formatMessage({id: 'notification_settings.push_threads.description', defaultMessage: 'Notify me about all replies to threads I\'m following'})}
selected={pushThread === 'all'}
type='toggle'
/>
<View style={styles.separator}/>
</Block>
<SettingSeparator/>
</SettingBlock>
);
};

View file

@ -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'),
};
});

View file

@ -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 (
<SafeAreaView
edges={edges}
testID='notification_settings.screen'
style={styles.container}
>
<ScrollView
contentContainerStyle={styles.wrapper}
alwaysBounceVertical={false}
>
<View style={styles.divider}/>
<SettingOption
defaultMessage={isCRTEnabled ? mentionTexts.crtOn.defaultMessage : mentionTexts.crtOff.defaultMessage}
i18nId={isCRTEnabled ? mentionTexts.crtOn.id : mentionTexts.crtOff.id}
onPress={goToNotificationSettingsMentions}
optionName='mentions'
/>
<SettingOption
optionName='push_notification'
onPress={goToNotificationSettingsPush}
/>
{enableAutoResponder && (
<SettingOption
onPress={goToNotificationAutoResponder}
optionName='automatic_dm_replies'
<SettingContainer>
<SettingItem
defaultMessage={isCRTEnabled ? mentionTexts.crtOn.defaultMessage : mentionTexts.crtOff.defaultMessage}
i18nId={isCRTEnabled ? mentionTexts.crtOn.id : mentionTexts.crtOff.id}
onPress={goToNotificationSettingsMentions}
optionName='mentions'
/>
<SettingItem
optionName='push_notification'
onPress={goToNotificationSettingsPush}
/>
<SettingItem
optionName='email'
onPress={goToEmailSettings}
rightComponent={
<SettingRowLabel
text={intl.formatMessage(getEmailIntervalTexts(emailIntervalPref))}
/>
)}
<View style={styles.divider}/>
</ScrollView>
</SafeAreaView>
}
/>
{enableAutoResponder && (
<SettingItem
onPress={goToNotificationAutoResponder}
optionName='automatic_dm_replies'
rightComponent={
<SettingRowLabel
text={currentUser.status === General.OUT_OF_OFFICE && notifyProps.auto_responder_active === 'true' ? 'On' : 'Off'}
/>
}
/>
)}
</SettingContainer>
);
};

View file

@ -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 (
<Block
headerText={headerText}
headerStyles={styles.blockHeader}
{...props}
>
{props.children}
</Block>
);
};
export default SettingBlock;

View file

@ -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 (
<SafeAreaView
edges={edges}
style={styles.container}
>
<ScrollView
contentContainerStyle={styles.contentContainerStyle}
alwaysBounceVertical={false}
>
{children}
</ScrollView>
</SafeAreaView>
);
};
export default SettingContainer;

View file

@ -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<MenuItemProps, 'testID'| 'theme'>;
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<MenuItemProps, 'onPress'| 'theme'>;
return (
<MenuItem
chevronStyle={styles.chevronStyle}
labelStyle={styles.menuLabel}
onPress={onPress}
separator={Platform.OS === 'ios'}
separatorStyle={styles.separatorStyle}
showArrow={Platform.select({ios: true, default: false})}
{...props}
/>
);
};
export default SettingItem;

View file

@ -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<MenuItemProps, 'testID'| 'theme'>;
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<MenuItemProps, 'onPress'| 'theme'>;
return (
<MenuItem
labelStyle={styles.menuLabel}
onPress={onPress}
separator={true}
showArrow={Platform.select({ios: true, default: false})}
theme={theme}
<OptionItem
optionDescriptionTextStyle={styles.optionDescriptionTextStyle}
optionLabelTextStyle={styles.optionLabelTextStyle}
containerStyle={[styles.container, props.description && {marginTop: 16}]}
{...props}
/>
);

View file

@ -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
style={styles.rightLabel}
>
{text}
</Text>
);
};
export default SettingRowLabel;

View file

@ -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 (
<SafeAreaView
edges={edges}
style={styles.container}
testID='account.screen'
>
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={styles.wrapper}
>
<View style={styles.divider}/>
<View
style={styles.group}
>
<SettingOption
optionName='notification'
onPress={goToNotifications}
/>
<SettingOption
optionName='display'
onPress={goToDisplaySettings}
/>
<SettingOption
optionName='advanced_settings'
onPress={goToAdvancedSettings}
/>
<SettingOption
optionName='about'
onPress={goToAbout}
messageValues={{appTitle: serverName}}
separator={Platform.OS === 'ios'}
/>
</View>
<View style={middleDividerStyle}/>
<View
style={styles.group}
>
{showHelp &&
<SettingOption
optionName='help'
onPress={openHelp}
isLink={true}
containerStyle={styles.innerContainerStyle}
/>
}
</View>
</ScrollView>
</SafeAreaView>
<SettingContainer >
<SettingItem
onPress={goToNotifications}
optionName='notification'
/>
<SettingItem
onPress={goToDisplaySettings}
optionName='display'
/>
<SettingItem
onPress={goToAdvancedSettings}
optionName='advanced_settings'
/>
<SettingItem
messageValues={{appTitle: serverName}}
onPress={goToAbout}
optionName='about'
/>
{Platform.OS === 'android' && <View style={styles.helpGroup}/>}
{showHelp &&
<SettingItem
containerStyle={styles.containerStyle}
isLink={true}
onPress={openHelp}
optionName='help'
separator={false}
/>
}
</SettingContainer>
);
};

View file

@ -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<ViewStyle>;
}
const SettingSeparator = ({lineStyles}: SettingSeparatorProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (<View style={[styles.separator, lineStyles]}/>);
};
export default SettingSeparator;

View file

@ -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<string, any> = {
[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];
};

View file

@ -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",

View file

@ -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 = {