feat: Channel notification preferences (#7160)

* feat: Channel notification preferences

* feedback review

* use button color for the icon
This commit is contained in:
Elias Nahum
2023-02-24 12:41:36 +02:00
committed by GitHub
parent c6dc00e4df
commit 2fc1386b78
46 changed files with 744 additions and 252 deletions

View File

@@ -1,88 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {MessageDescriptor} from '@formatjs/intl/src/types';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginBottom: 30,
},
header: {
marginHorizontal: 15,
marginBottom: 10,
fontSize: 13,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
footer: {
marginTop: 10,
marginHorizontal: 15,
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
};
});
export type SectionText = {
id: string;
defaultMessage: string;
values?: MessageDescriptor;
}
export type BlockProps = {
children: React.ReactNode;
disableFooter?: boolean;
disableHeader?: boolean;
footerText?: SectionText;
headerText?: SectionText;
containerStyles?: StyleProp<ViewStyle>;
headerStyles?: StyleProp<TextStyle>;
footerStyles?: StyleProp<TextStyle>;
}
const Block = ({
children,
containerStyles,
disableFooter,
disableHeader,
footerText,
headerStyles,
headerText,
footerStyles,
}: BlockProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<View style={styles.container}>
{(headerText && !disableHeader) &&
<FormattedText
defaultMessage={headerText.defaultMessage}
id={headerText.id}
values={headerText.values}
style={[styles.header, headerStyles]}
/>
}
<View style={containerStyles}>
{children}
</View>
{(footerText && !disableFooter) &&
<FormattedText
defaultMessage={footerText.defaultMessage}
id={footerText.id}
style={[styles.footer, footerStyles]}
values={footerText.values}
/>
}
</View>
);
};
export default Block;

View File

@@ -2,12 +2,15 @@
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
import RNButton from 'react-native-button';
import CompassIcon from '@components/compass_icon';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
type Props = {
type ConditionalProps = | {iconName: string; iconSize: number} | {iconName?: never; iconSize?: never}
type Props = ConditionalProps & {
theme: Theme;
backgroundStyle?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
@@ -20,6 +23,11 @@ type Props = {
text: string;
}
const styles = StyleSheet.create({
container: {flexDirection: 'row'},
icon: {marginRight: 7},
});
const Button = ({
theme,
backgroundStyle,
@@ -31,6 +39,8 @@ const Button = ({
onPress,
text,
testID,
iconName,
iconSize,
}: Props) => {
const bgStyle = useMemo(() => [
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
@@ -48,12 +58,22 @@ const Button = ({
onPress={onPress}
testID={testID}
>
<Text
style={txtStyle}
numberOfLines={1}
>
{text}
</Text>
<View style={styles.container}>
{Boolean(iconName) &&
<CompassIcon
name={iconName!}
size={iconSize}
color={StyleSheet.flatten(txtStyle).color}
style={styles.icon}
/>
}
<Text
style={txtStyle}
numberOfLines={1}
>
{text}
</Text>
</View>
</RNButton>
);
};

View File

@@ -10,7 +10,7 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannelsWithCalls} from '@calls/state';
import {General} from '@constants';
import {withServerUrl} from '@context/server';
import {observeChannelSettings, observeMyChannel, queryChannelMembers} from '@queries/servers/channel';
import {observeIsMutedSetting, observeMyChannel, queryChannelMembers} from '@queries/servers/channel';
import {queryDraft} from '@queries/servers/drafts';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -19,7 +19,6 @@ import ChannelItem from './channel_item';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
type EnhanceProps = WithDatabaseArgs & {
channel: ChannelModel;
@@ -27,8 +26,6 @@ type EnhanceProps = WithDatabaseArgs & {
serverUrl?: string;
}
const observeIsMutedSetting = (mc: MyChannelModel) => observeChannelSettings(mc.database, mc.id).pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
const enhance = withObservables(['channel', 'showTeamName'], ({
channel,
database,
@@ -53,7 +50,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
if (!mc) {
return of$(false);
}
return observeIsMutedSetting(mc);
return observeIsMutedSetting(database, mc.id);
}),
);

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {MessageDescriptor} from 'react-intl';
type SectionText = {
id: string;
defaultMessage: string;
values?: MessageDescriptor;
}
type SettingBlockProps = {
children: React.ReactNode;
containerStyles?: StyleProp<ViewStyle>;
disableFooter?: boolean;
disableHeader?: boolean;
footerStyles?: StyleProp<TextStyle>;
footerText?: SectionText;
headerStyles?: StyleProp<TextStyle>;
headerText?: SectionText;
onLayout?: (event: LayoutChangeEvent) => void;
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginBottom: 30,
},
contentContainerStyle: {
marginBottom: 0,
},
header: {
color: theme.centerChannelColor,
...typography('Heading', 300, 'SemiBold'),
marginBottom: 8,
marginLeft: 20,
marginTop: 12,
marginRight: 15,
},
footer: {
marginTop: 10,
marginHorizontal: 15,
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
};
});
const SettingBlock = ({
children, containerStyles, disableFooter, disableHeader,
footerStyles, footerText, headerStyles, headerText, onLayout,
}: SettingBlockProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
return (
<View
style={styles.container}
onLayout={onLayout}
>
{(headerText && !disableHeader) &&
<FormattedText
defaultMessage={headerText.defaultMessage}
id={headerText.id}
values={headerText.values}
style={[styles.header, headerStyles]}
/>
}
<View style={[styles.contentContainerStyle, containerStyles]}>
{children}
</View>
{(footerText && !disableFooter) &&
<FormattedText
defaultMessage={footerText.defaultMessage}
id={footerText.id}
style={[styles.footer, footerStyles]}
values={footerText.values}
/>
}
</View>
);
};
export default SettingBlock;

View File

@@ -7,11 +7,12 @@ import {Platform} from 'react-native';
import OptionItem, {OptionItemProps} from '@components/option_item';
import {useTheme} from '@context/theme';
import SettingSeparator from '@screens/settings/settings_separator';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import Options, {DisplayOptionConfig, NotificationsOptionConfig, SettingOptionConfig} from './config';
import Options, {DisplayOptionConfig, NotificationsOptionConfig, SettingOptionConfig} from '../../screens/settings/config';
import SettingSeparator from './separator';
type SettingsConfig = keyof typeof SettingOptionConfig | keyof typeof NotificationsOptionConfig| keyof typeof DisplayOptionConfig
type SettingOptionProps = {

View File

@@ -10,7 +10,7 @@ export const CALL = 'Call';
export const CHANNEL = 'Channel';
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
export const CHANNEL_INFO = 'ChannelInfo';
export const CHANNEL_MENTION = 'ChannelMention';
export const CHANNEL_NOTIFICATION_PREFERENCES = 'ChannelNotificationPreferences';
export const CODE = 'Code';
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
@@ -79,7 +79,7 @@ export default {
CHANNEL,
CHANNEL_ADD_PEOPLE,
CHANNEL_INFO,
CHANNEL_MENTION,
CHANNEL_NOTIFICATION_PREFERENCES,
CODE,
CREATE_DIRECT_MESSAGE,
CREATE_OR_EDIT_CHANNEL,
@@ -172,6 +172,5 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
export const NOT_READY = [
CHANNEL_ADD_PEOPLE,
CHANNEL_MENTION,
CREATE_TEAM,
];

View File

@@ -628,6 +628,10 @@ export const observeChannelSettings = (database: Database, channelId: string) =>
);
};
export const observeIsMutedSetting = (database: Database, channelId: string) => {
return observeChannelSettings(database, channelId).pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
};
export const observeChannelsByLastPostAt = (database: Database, myChannels: MyChannelModel[], excludeIds?: string[]) => {
const ids = myChannels.map((c) => c.id);
const idsStr = `'${ids.join("','")}'`;

View File

@@ -10,6 +10,7 @@ import {isTypeDMorGM} from '@utils/channel';
import EditChannel from './edit_channel';
import IgnoreMentions from './ignore_mentions';
import Members from './members';
import NotificationPreference from './notification_preference';
import PinnedMessages from './pinned_messages';
type Props = {
@@ -26,7 +27,7 @@ const Options = ({channelId, type, callsEnabled}: Props) => {
{type !== General.DM_CHANNEL &&
<IgnoreMentions channelId={channelId}/>
}
{/*<NotificationPreference channelId={channelId}/>*/}
<NotificationPreference channelId={channelId}/>
<PinnedMessages channelId={channelId}/>
{type !== General.DM_CHANNEL &&
<Members channelId={channelId}/>

View File

@@ -6,7 +6,9 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannelSettings} from '@queries/servers/channel';
import {observeChannel, observeChannelSettings} from '@queries/servers/channel';
import {observeCurrentUser} from '@queries/servers/user';
import {getNotificationProps} from '@utils/user';
import NotificationPreference from './notification_preference';
@@ -17,13 +19,17 @@ type Props = WithDatabaseArgs & {
}
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
const displayName = observeChannel(database, channelId).pipe(switchMap((c) => of$(c?.displayName)));
const settings = observeChannelSettings(database, channelId);
const userNotifyLevel = observeCurrentUser(database).pipe(switchMap((u) => of$(getNotificationProps(u).push)));
const notifyLevel = settings.pipe(
switchMap((s) => of$(s?.notifyProps.push)),
);
return {
displayName,
notifyLevel,
userNotifyLevel,
};
});

View File

@@ -7,54 +7,85 @@ import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {NotificationLevel, Screens} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
import type {Options} from 'react-native-navigation';
type Props = {
channelId: string;
displayName: string;
notifyLevel: NotificationLevel;
userNotifyLevel: NotificationLevel;
}
const NotificationPreference = ({channelId, notifyLevel}: Props) => {
const notificationLevel = (notifyLevel: NotificationLevel) => {
let id = '';
let defaultMessage = '';
switch (notifyLevel) {
case NotificationLevel.ALL: {
id = t('channel_info.notification.all');
defaultMessage = 'All';
break;
}
case NotificationLevel.MENTION: {
id = t('channel_info.notification.mention');
defaultMessage = 'Mentions';
break;
}
case NotificationLevel.NONE: {
id = t('channel_info.notification.none');
defaultMessage = 'Never';
break;
}
default:
id = t('channel_info.notification.default');
defaultMessage = 'Default';
break;
}
return {id, defaultMessage};
};
const NotificationPreference = ({channelId, displayName, notifyLevel, userNotifyLevel}: Props) => {
const {formatMessage} = useIntl();
const theme = useTheme();
const title = formatMessage({id: 'channel_info.mobile_notifications', defaultMessage: 'Mobile Notifications'});
const goToMentions = preventDoubleTap(() => {
goToScreen(Screens.CHANNEL_MENTION, title, {channelId});
const goToChannelNotificationPreferences = preventDoubleTap(() => {
const options: Options = {
topBar: {
title: {
text: title,
},
subtitle: {
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
text: displayName,
},
backButton: {
popStackOnPress: false,
},
},
};
goToScreen(Screens.CHANNEL_NOTIFICATION_PREFERENCES, title, {channelId}, options);
});
const notificationLevelToText = () => {
let id = '';
let defaultMessage = '';
switch (notifyLevel) {
case NotificationLevel.ALL: {
id = t('channel_info.notification.all');
defaultMessage = 'All';
break;
}
case NotificationLevel.MENTION: {
id = t('channel_info.notification.mention');
defaultMessage = 'Mentions';
break;
}
case NotificationLevel.NONE: {
id = t('channel_info.notification.none');
defaultMessage = 'Never';
break;
}
default:
id = t('channel_info.notification.default');
defaultMessage = 'Default';
break;
if (notifyLevel === NotificationLevel.DEFAULT) {
const userLevel = notificationLevel(userNotifyLevel);
return formatMessage(userLevel);
}
return formatMessage({id, defaultMessage});
const channelLevel = notificationLevel(notifyLevel);
return formatMessage(channelLevel);
};
return (
<OptionItem
action={goToMentions}
action={goToChannelNotificationPreferences}
label={title}
icon='cellphone'
type={Platform.select({ios: 'arrow', default: 'default'})}

View File

@@ -0,0 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react';
import {LayoutAnimation} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
import {updateChannelNotifyProps} from '@actions/remote/channel';
import SettingsContainer from '@components/settings/container';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useDidUpdate from '@hooks/did_update';
import useBackNavigation from '@hooks/navigate_back';
import {popTopScreen} from '../navigation';
import MutedBanner, {MUTED_BANNER_HEIGHT} from './muted_banner';
import NotifyAbout, {BLOCK_TITLE_HEIGHT} from './notify_about';
import ResetToDefault from './reset';
import ThreadReplies from './thread_replies';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
channelId: string;
componentId: AvailableScreens;
defaultLevel: NotificationLevel;
defaultThreadReplies: 'all' | 'mention';
isCRTEnabled: boolean;
isMuted: boolean;
notifyLevel?: NotificationLevel;
notifyThreadReplies?: 'all' | 'mention';
}
const ChannelNotificationPreferences = ({channelId, componentId, defaultLevel, defaultThreadReplies, isCRTEnabled, isMuted, notifyLevel, notifyThreadReplies}: Props) => {
const serverUrl = useServerUrl();
const defaultNotificationReplies = defaultThreadReplies === 'all';
const diffNotificationLevel = notifyLevel !== 'default' && notifyLevel !== defaultLevel;
const notifyTitleTop = useSharedValue((isMuted ? MUTED_BANNER_HEIGHT : 0) + BLOCK_TITLE_HEIGHT);
const [notifyAbout, setNotifyAbout] = useState<NotificationLevel>((notifyLevel === undefined || notifyLevel === 'default') ? defaultLevel : notifyLevel);
const [threadReplies, setThreadReplies] = useState<boolean>((notifyThreadReplies || defaultThreadReplies) === 'all');
const [resetDefaultVisible, setResetDefaultVisible] = useState(diffNotificationLevel || defaultNotificationReplies !== threadReplies);
useDidUpdate(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
}, [isMuted]);
const onResetPressed = useCallback(() => {
setResetDefaultVisible(false);
setNotifyAbout(defaultLevel);
setThreadReplies(defaultNotificationReplies);
}, [defaultLevel, defaultNotificationReplies]);
const onNotificationLevel = useCallback((level: NotificationLevel) => {
setNotifyAbout(level);
setResetDefaultVisible(level !== defaultLevel || defaultNotificationReplies !== threadReplies);
}, [defaultLevel, defaultNotificationReplies, threadReplies]);
const onSetThreadReplies = useCallback((value: boolean) => {
setThreadReplies(value);
setResetDefaultVisible(defaultNotificationReplies !== value || notifyAbout !== defaultLevel);
}, [defaultLevel, defaultNotificationReplies, notifyAbout]);
const save = useCallback(() => {
const pushThreads = threadReplies ? 'all' : 'mention';
if (notifyLevel !== notifyAbout || (isCRTEnabled && pushThreads !== notifyThreadReplies)) {
const props: Partial<ChannelNotifyProps> = {push: notifyAbout};
if (isCRTEnabled) {
props.push_threads = pushThreads;
}
updateChannelNotifyProps(serverUrl, channelId, props);
}
popTopScreen(componentId);
}, [channelId, componentId, isCRTEnabled, notifyAbout, notifyLevel, notifyThreadReplies, serverUrl, threadReplies]);
useBackNavigation(save);
useAndroidHardwareBackHandler(componentId, save);
return (
<SettingsContainer testID='push_notification_settings'>
{isMuted && <MutedBanner channelId={channelId}/>}
{resetDefaultVisible &&
<ResetToDefault
onPress={onResetPressed}
topPosition={notifyTitleTop}
/>
}
<NotifyAbout
defaultLevel={defaultLevel}
isMuted={isMuted}
notifyLevel={notifyAbout}
notifyTitleTop={notifyTitleTop}
onPress={onNotificationLevel}
/>
{isCRTEnabled &&
<ThreadReplies
isSelected={threadReplies}
onPress={onSetThreadReplies}
notifyLevel={notifyAbout}
/>
}
</SettingsContainer>
);
};
export default ChannelNotificationPreferences;

View File

@@ -0,0 +1,53 @@
// 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 {observeChannelSettings, observeIsMutedSetting} from '@queries/servers/channel';
import {observeIsCRTEnabled} from '@queries/servers/thread';
import {observeCurrentUser} from '@queries/servers/user';
import {getNotificationProps} from '@utils/user';
import ChannelNotificationPreferences from './channel_notification_preferences';
import type {WithDatabaseArgs} from '@typings/database/database';
type EnhancedProps = WithDatabaseArgs & {
channelId: string;
}
const enhanced = withObservables([], ({channelId, database}: EnhancedProps) => {
const settings = observeChannelSettings(database, channelId);
const isCRTEnabled = observeIsCRTEnabled(database);
const isMuted = observeIsMutedSetting(database, channelId);
const notifyProps = observeCurrentUser(database).pipe(switchMap((u) => of$(getNotificationProps(u))));
const notifyLevel = settings.pipe(
switchMap((s) => of$(s?.notifyProps.push)),
);
const notifyThreadReplies = settings.pipe(
switchMap((s) => of$(s?.notifyProps.push_threads)),
);
const defaultLevel = notifyProps.pipe(
switchMap((n) => of$(n?.push)),
);
const defaultThreadReplies = notifyProps.pipe(
switchMap((n) => of$(n?.push_threads)),
);
return {
isCRTEnabled,
isMuted,
notifyLevel,
notifyThreadReplies,
defaultLevel,
defaultThreadReplies,
};
});
export default withDatabase(enhanced(ChannelNotificationPreferences));

View File

@@ -0,0 +1,102 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {View} from 'react-native';
import Animated, {FlipOutXUp} from 'react-native-reanimated';
import {toggleMuteChannel} from '@actions/remote/channel';
import Button from '@app/components/button';
import CompassIcon from '@app/components/compass_icon';
import FormattedText from '@app/components/formatted_text';
import {useServerUrl} from '@app/context/server';
import {useTheme} from '@app/context/theme';
import {preventDoubleTap} from '@app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme';
import {typography} from '@utils/typography';
type Props = {
channelId: string;
}
export const MUTED_BANNER_HEIGHT = 200;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
button: {width: '55%'},
container: {
backgroundColor: changeOpacity(theme.sidebarTextActiveBorder, 0.16),
borderRadius: 4,
marginHorizontal: 20,
marginVertical: 12,
paddingHorizontal: 16,
height: MUTED_BANNER_HEIGHT,
},
contentText: {
...typography('Body', 200),
color: theme.centerChannelColor,
marginTop: 12,
marginBottom: 16,
},
titleContainer: {
alignItems: 'center',
flexDirection: 'row',
marginTop: 16,
},
title: {
...typography('Heading', 200),
color: theme.centerChannelColor,
marginLeft: 10,
paddingTop: 5,
},
}));
const MutedBanner = ({channelId}: Props) => {
const {formatMessage} = useIntl();
const serverUrl = useServerUrl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(() => {
toggleMuteChannel(serverUrl, channelId, false);
}), [channelId, serverUrl]);
return (
<Animated.View
exiting={FlipOutXUp}
style={styles.container}
>
<View style={styles.titleContainer}>
<CompassIcon
name='bell-off-outline'
size={24}
color={theme.linkColor}
/>
<FormattedText
id='channel_notification_preferences.muted_title'
defaultMessage='This channel is muted'
style={styles.title}
/>
</View>
<FormattedText
id='channel_notification_preferences.muted_content'
defaultMessage='You can change the notification settings, but you will not receive notifications until the channel is unmuted.'
style={styles.contentText}
/>
<Button
buttonType='default'
onPress={onPress}
text={formatMessage({
id: 'channel_notification_preferences.unmute_content',
defaultMessage: 'Unmute channel',
})}
theme={theme}
backgroundStyle={styles.button}
iconName='bell-outline'
iconSize={18}
/>
</Animated.View>
);
};
export default MutedBanner;

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {LayoutChangeEvent, View} from 'react-native';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {NotificationLevel} from '@constants';
import {t} from '@i18n';
import type {SharedValue} from 'react-native-reanimated';
type Props = {
isMuted: boolean;
defaultLevel: NotificationLevel;
notifyLevel: NotificationLevel;
notifyTitleTop: SharedValue<number>;
onPress: (level: NotificationLevel) => void;
}
type NotifPrefOptions = {
defaultMessage: string;
id: string;
testID: string;
value: string;
}
export const BLOCK_TITLE_HEIGHT = 13;
const NOTIFY_ABOUT = {id: t('channel_notification_preferences.notify_about'), defaultMessage: 'Notify me about...'};
const NOTIFY_OPTIONS: Record<string, NotifPrefOptions> = {
[NotificationLevel.ALL]: {
defaultMessage: 'All new messages',
id: t('channel_notification_preferences.notification.all'),
testID: 'channel_notification_preferences.notification.all',
value: NotificationLevel.ALL,
},
[NotificationLevel.MENTION]: {
defaultMessage: 'Mentions, direct messages only',
id: t('channel_notification_preferences.notification.mention'),
testID: 'channel_notification_preferences.notification.mention',
value: NotificationLevel.MENTION,
},
[NotificationLevel.NONE]: {
defaultMessage: 'Nothing',
id: t('channel_notification_preferences.notification.none'),
testID: 'channel_notification_preferences.notification.none',
value: NotificationLevel.NONE,
},
};
const NotifyAbout = ({defaultLevel, isMuted, notifyLevel, notifyTitleTop, onPress}: Props) => {
const {formatMessage} = useIntl();
const onLayout = useCallback((e: LayoutChangeEvent) => {
const {y} = e.nativeEvent.layout;
notifyTitleTop.value = y > 0 ? y + 10 : BLOCK_TITLE_HEIGHT;
}, []);
return (
<SettingBlock
headerText={NOTIFY_ABOUT}
headerStyles={{marginTop: isMuted ? 8 : 12}}
onLayout={onLayout}
>
{Object.keys(NOTIFY_OPTIONS).map((key) => {
const {id, defaultMessage, value, testID} = NOTIFY_OPTIONS[key];
const defaultOption = key === defaultLevel ? formatMessage({id: 'channel_notification_preferences.default', defaultMessage: '(default)'}) : '';
const label = `${formatMessage({id, defaultMessage})} ${defaultOption}`;
return (
<View key={`notif_pref_option${key}`}>
<SettingOption
action={onPress}
label={label}
selected={notifyLevel === key}
testID={testID}
type='select'
value={value}
/>
<SettingSeparator/>
</View>
);
})}
</SettingBlock>
);
};
export default NotifyAbout;

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {TouchableOpacity} from 'react-native';
import Animated, {SharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
onPress: () => void;
topPosition: SharedValue<number>;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
position: 'absolute',
right: 20,
zIndex: 1,
},
row: {flexDirection: 'row'},
text: {
color: theme.linkColor,
marginLeft: 7,
...typography('Heading', 100),
},
}));
const ResetToDefault = ({onPress, topPosition}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const animatedStyle = useAnimatedStyle(() => ({
top: withTiming(topPosition.value, {duration: 100}),
}));
return (
<Animated.View style={[styles.container, animatedStyle]}>
<TouchableOpacity
onPress={onPress}
style={styles.row}
>
<CompassIcon
name='refresh'
size={18}
color={theme.linkColor}
/>
<FormattedText
id='channel_notification_preferences.reset_default'
defaultMessage='Reset to default'
style={styles.text}
/>
</TouchableOpacity>
</Animated.View>
);
};
export default ResetToDefault;

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {NotificationLevel} from '@constants';
import {t} from '@i18n';
type Props = {
isSelected: boolean;
notifyLevel: NotificationLevel;
onPress: (selected: boolean) => void;
}
type NotifPrefOptions = {
defaultMessage: string;
id: string;
testID: string;
value: string;
}
const THREAD_REPLIES = {id: t('channel_notification_preferences.thread_replies'), defaultMessage: 'Thread replies'};
const NOTIFY_OPTIONS_THREAD: Record<string, NotifPrefOptions> = {
THREAD_REPLIES: {
defaultMessage: 'Notify me about replies to threads Im following in this channel',
id: t('channel_notification_preferences.notification.thread_replies'),
testID: 'channel_notification_preferences.notification.thread_replies',
value: 'thread_replies',
},
};
const NotifyAbout = ({isSelected, notifyLevel, onPress}: Props) => {
const {formatMessage} = useIntl();
if ([NotificationLevel.NONE, NotificationLevel.ALL].includes(notifyLevel)) {
return null;
}
return (
<SettingBlock headerText={THREAD_REPLIES}>
<SettingOption
action={onPress}
label={formatMessage({id: NOTIFY_OPTIONS_THREAD.THREAD_REPLIES.id, defaultMessage: NOTIFY_OPTIONS_THREAD.THREAD_REPLIES.defaultMessage})}
testID={NOTIFY_OPTIONS_THREAD.THREAD_REPLIES.testID}
type='toggle'
selected={isSelected}
/>
<SettingSeparator/>
</SettingBlock>
);
};
export default NotifyAbout;

View File

@@ -79,6 +79,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.CHANNEL:
screen = withServerDatabase(require('@screens/channel').default);
break;
case Screens.CHANNEL_NOTIFICATION_PREFERENCES:
screen = withServerDatabase(require('@screens/channel_notification_preferences').default);
break;
case Screens.CHANNEL_INFO:
screen = withServerDatabase(require('@screens/channel_info').default);
break;

View File

@@ -9,13 +9,13 @@ import DeviceInfo from 'react-native-device-info';
import Config from '@assets/config.json';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import SettingContainer from '@components/settings/container';
import SettingSeparator from '@components/settings/separator';
import AboutLinks from '@constants/about_links';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {t} from '@i18n';
import {popTopScreen} from '@screens/navigation';
import SettingContainer from '@screens/settings/setting_container';
import SettingSeparator from '@screens/settings/settings_separator';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';

View File

@@ -5,18 +5,17 @@ import React, {useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {Alert, TouchableOpacity} from 'react-native';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {popTopScreen} from '@screens/navigation';
import SettingSeparator from '@screens/settings/settings_separator';
import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme} from '@utils/theme';
import SettingContainer from '../setting_container';
import SettingOption from '../setting_option';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {ReadDirItem} from 'react-native-fs';

View File

@@ -4,6 +4,8 @@
import React, {useMemo} from 'react';
import {useIntl} from 'react-intl';
import SettingContainer from '@components/settings/container';
import SettingItem from '@components/settings/item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@@ -13,9 +15,6 @@ import {gotoSettingsScreen} from '@screens/settings/config';
import {preventDoubleTap} from '@utils/tap';
import {getUserTimezoneProps} from '@utils/user';
import SettingContainer from '../setting_container';
import SettingItem from '../setting_item';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -5,17 +5,16 @@ import React, {useCallback, useState} from 'react';
import {useIntl} from 'react-intl';
import {savePreference} from '@actions/remote/preference';
import SettingBlock from '@components/settings/block';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useBackNavigation from '@hooks/navigate_back';
import {popTopScreen} from '@screens/navigation';
import SettingBlock from '../setting_block';
import SettingContainer from '../setting_container';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
import type {AvailableScreens} from '@typings/screens/navigation';
const CLOCK_TYPE = {

View File

@@ -5,6 +5,10 @@ import React, {useCallback, useState} from 'react';
import {useIntl} from 'react-intl';
import {savePreference} from '@actions/remote/preference';
import SettingBlock from '@components/settings/block';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@@ -12,11 +16,6 @@ import useBackNavigation from '@hooks/navigate_back';
import {t} from '@i18n';
import {popTopScreen} from '@screens/navigation';
import SettingBlock from '../setting_block';
import SettingContainer from '../setting_container';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
import type {AvailableScreens} from '@typings/screens/navigation';
const crtDescription = {

View File

@@ -4,10 +4,9 @@
import React from 'react';
import {useIntl} from 'react-intl';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {useTheme} from '@context/theme';
import SettingSeparator from '@screens/settings/settings_separator';
import SettingOption from '../setting_option';
const radioItemProps = {checkedBody: true};

View File

@@ -4,14 +4,13 @@
import React, {useCallback, useMemo} from 'react';
import {savePreference} from '@actions/remote/preference';
import SettingContainer from '@components/settings/container';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {popTopScreen} from '@screens/navigation';
import SettingContainer from '../setting_container';
import CustomTheme from './custom_theme';
import {ThemeTiles} from './theme_tiles';

View File

@@ -5,6 +5,9 @@ import React, {useCallback, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {updateMe} from '@actions/remote/user';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@@ -14,10 +17,6 @@ import {preventDoubleTap} from '@utils/tap';
import {getDeviceTimezone} from '@utils/timezone';
import {getTimezoneRegion, getUserTimezoneProps} from '@utils/user';
import SettingContainer from '../setting_container';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -5,8 +5,8 @@ import React, {useCallback} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';
import CompassIcon from '@components/compass_icon';
import SettingSeparator from '@components/settings/separator';
import {useTheme} from '@context/theme';
import SettingSeparator from '@screens/settings/settings_separator';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';

View File

@@ -7,6 +7,9 @@ import {useIntl} from 'react-intl';
import {fetchStatusInBatch, updateMe} from '@actions/remote/user';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@@ -18,10 +21,6 @@ import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme}
import {typography} from '@utils/typography';
import {getNotificationProps} from '@utils/user';
import SettingContainer from '../setting_container';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -7,6 +7,10 @@ import {Text} from 'react-native';
import {savePreference} from '@actions/remote/preference';
import {updateMe} from '@actions/remote/user';
import SettingBlock from '@components/settings/block';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {Preferences} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@@ -18,11 +22,6 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {getEmailInterval, getNotificationProps} from '@utils/user';
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';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -7,6 +7,9 @@ import {Text} from 'react-native';
import {updateMe} from '@actions/remote/user';
import FloatingTextInput from '@components/floating_text_input_label';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@@ -18,10 +21,6 @@ import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme}
import {typography} from '@utils/typography';
import {getNotificationProps} from '@utils/user';
import SettingBlock from '../setting_block';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -3,7 +3,7 @@
import React from 'react';
import SettingContainer from '../setting_container';
import SettingContainer from '@components/settings/container';
import MentionSettings from './mention_settings';

View File

@@ -4,12 +4,11 @@
import React, {Dispatch, SetStateAction} from 'react';
import {useIntl} from 'react-intl';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {t} from '@i18n';
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',

View File

@@ -5,15 +5,14 @@ import React, {useCallback, useMemo, useState} from 'react';
import {Platform} from 'react-native';
import {updateMe} from '@actions/remote/user';
import SettingContainer from '@components/settings/container';
import SettingSeparator from '@components/settings/separator';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useBackNavigation from '@hooks/navigate_back';
import {popTopScreen} from '@screens/navigation';
import SettingSeparator from '@screens/settings/settings_separator';
import {getNotificationProps} from '@utils/user';
import SettingContainer from '../setting_container';
import MobileSendPush from './push_send';
import MobilePushStatus from './push_status';
import MobilePushThread from './push_thread';

View File

@@ -5,15 +5,14 @@ import React from 'react';
import {useIntl} from 'react-intl';
import FormattedText from '@components/formatted_text';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
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.about'),
defaultMessage: 'Notify me about...',

View File

@@ -4,12 +4,11 @@
import React from 'react';
import {useIntl} from 'react-intl';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {t} from '@i18n';
import SettingBlock from '../setting_block';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
const headerText = {
id: t('notification_settings.mobile.trigger_push'),
defaultMessage: 'Trigger push notifications when...',

View File

@@ -4,12 +4,11 @@
import React from 'react';
import {useIntl} from 'react-intl';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {t} from '@i18n';
import SettingBlock from '../setting_block';
import SettingOption from '../setting_option';
import SettingSeparator from '../settings_separator';
const headerText = {
id: t('notification_settings.push_threads.replies'),
defaultMessage: 'Thread replies',

View File

@@ -4,6 +4,8 @@
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import SettingContainer from '@components/settings/container';
import SettingItem from '@components/settings/item';
import {General, Screens} from '@constants';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {t} from '@i18n';
@@ -11,9 +13,6 @@ import {popTopScreen} from '@screens/navigation';
import {gotoSettingsScreen} from '@screens/settings/config';
import {getEmailInterval, getEmailIntervalTexts, getNotificationProps} from '@utils/user';
import SettingContainer from '../setting_container';
import SettingItem from '../setting_item';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -7,11 +7,10 @@ import React from 'react';
import {Alert, Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import SettingItem from '@components/settings/item';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import SettingItem from '../setting_item';
type ReportProblemProps = {
buildNumber: string;
currentTeamId: string;

View File

@@ -1,46 +0,0 @@
// 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: 8,
marginLeft: 20,
marginTop: 12,
},
contentContainerStyle: {
marginBottom: 0,
},
};
});
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}
containerStyles={styles.contentContainerStyle}
{...props}
>
{props.children}
</Block>
);
};
export default SettingBlock;

View File

@@ -6,19 +6,19 @@ import {useIntl} from 'react-intl';
import {Alert, Platform, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import SettingContainer from '@components/settings/container';
import SettingItem from '@components/settings/item';
import {Screens} from '@constants';
import {useServerDisplayName} from '@context/server';
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 {tryOpenURL} from '@utils/url';
import ReportProblem from './report_problem';
import SettingItem from './setting_item';
import type {AvailableScreens} from '@typings/screens/navigation';

View File

@@ -316,7 +316,7 @@ export function filterProfilesMatchingTerm(users: UserProfile[], term: string):
});
}
export function getNotificationProps(user: UserModel) {
export function getNotificationProps(user?: UserModel) {
if (user && user.notifyProps) {
return user.notifyProps;
}

View File

@@ -168,6 +168,17 @@
"channel_modal.optional": "(optional)",
"channel_modal.purpose": "Purpose",
"channel_modal.purposeEx": "A channel to file bugs and improvements",
"channel_notification_preferences.default": "(default)",
"channel_notification_preferences.muted_content": "You can change the notification settings, but you will not receive notifications until the channel is unmuted.",
"channel_notification_preferences.muted_title": "This channel is muted",
"channel_notification_preferences.notification.all": "All new messages",
"channel_notification_preferences.notification.mention": "Mentions, direct messages only",
"channel_notification_preferences.notification.none": "Nothing",
"channel_notification_preferences.notification.thread_replies": "Notify me about replies to threads Im following in this channel",
"channel_notification_preferences.notify_about": "Notify me about...",
"channel_notification_preferences.reset_default": "Reset to default",
"channel_notification_preferences.thread_replies": "Thread replies",
"channel_notification_preferences.unmute_content": "Unmute channel",
"combined_system_message.added_to_channel.many_expanded": "{users} and {lastUser} were **added to the channel** by {actor}.",
"combined_system_message.added_to_channel.one": "{firstUser} **added to the channel** by {actor}.",
"combined_system_message.added_to_channel.one_you": "You were **added to the channel** by {actor}.",

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import TurboLogger from '@mattermost/react-native-turbo-log';
import {LogBox, Platform} from 'react-native';
import {LogBox, Platform, UIManager} from 'react-native';
import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes';
import {RUNNING_E2E} from 'react-native-dotenv';
import 'react-native-gesture-handler';
@@ -53,6 +53,9 @@ if (Platform.OS === 'android') {
const ShareExtension = require('share_extension/index.tsx').default;
const AppRegistry = require('react-native/Libraries/ReactNative/AppRegistry');
AppRegistry.registerComponent('MattermostShare', () => ShareExtension);
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
Navigation.events().registerAppLaunchedListener(async () => {

View File

@@ -16,6 +16,7 @@ type ChannelNotifyProps = {
mark_unread: 'all' | 'mention';
push: NotificationLevel;
ignore_channel_mentions: 'default' | 'off' | 'on';
push_threads: 'all' | 'mention';
};
type Channel = {
id: string;