Merge branch 'main' into gm_to_channel

This commit is contained in:
harshil Sharma 2023-11-16 13:23:27 +05:30
commit ba9a84e3e6
17 changed files with 223 additions and 33 deletions

View file

@ -7,7 +7,7 @@ import Animated, {interpolate, useAnimatedStyle, useSharedValue, withSpring} fro
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {resetMessageCount} from '@actions/local/channel';
import {useCallsAdjustment} from '@app/products/calls/hooks';
import {useCallsAdjustment} from '@calls/hooks';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import TouchableWithFeedback from '@components/touchable_with_feedback';

View file

@ -6,6 +6,8 @@ export const MAX_CHANNEL_NAME_LENGTH = 64;
export const IGNORE_CHANNEL_MENTIONS_ON = 'on';
export const IGNORE_CHANNEL_MENTIONS_OFF = 'off';
export const IGNORE_CHANNEL_MENTIONS_DEFAULT = 'default';
export const CHANNEL_AUTO_FOLLOW_THREADS_TRUE = 'on';
export const CHANNEL_AUTO_FOLLOW_THREADS_FALSE = 'off';
export default {
IGNORE_CHANNEL_MENTIONS_ON,
@ -13,4 +15,6 @@ export default {
IGNORE_CHANNEL_MENTIONS_DEFAULT,
MAX_CHANNEL_NAME_LENGTH,
MIN_CHANNEL_NAME_LENGTH,
CHANNEL_AUTO_FOLLOW_THREADS_TRUE,
CHANNEL_AUTO_FOLLOW_THREADS_FALSE,
};

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} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {Pressable, Text, View} from 'react-native';
@ -18,11 +18,14 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import WebsocketManager from '@managers/websocket_manager';
import {getServerDisplayName} from '@queries/app/servers';
import ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
import type ServersModel from '@typings/database/models/app/servers';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
outerContainer: {
borderRadius: 8,
@ -39,6 +42,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
shadowRadius: 4,
elevation: 4,
},
outerContainerServerName: {
height: CALL_NOTIFICATION_BAR_HEIGHT + 8,
},
innerContainer: {
flexDirection: 'row',
width: '100%',
@ -71,6 +77,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
textContainer: {
flex: 1,
flexDirection: 'column',
marginLeft: 8,
},
text: {
@ -81,6 +88,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
...typography('Body', 100, 'SemiBold'),
lineHeight: 20,
},
textServerName: {
...typography('Heading', 25),
color: changeOpacity(theme.buttonColor, 0.72),
textTransform: 'uppercase',
},
dismissContainer: {
alignItems: 'center',
width: 32,
@ -93,6 +105,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
}));
type Props = {
servers: ServersModel[];
incomingCall: IncomingCallNotification;
currentUserId: string;
teammateNameDisplay: string;
@ -101,6 +114,7 @@ type Props = {
}
export const CallNotification = ({
servers,
incomingCall,
currentUserId,
teammateNameDisplay,
@ -111,6 +125,8 @@ export const CallNotification = ({
const serverUrl = useServerUrl();
const theme = useTheme();
const style = getStyleSheet(theme);
const [serverName, setServerName] = useState('');
const moreThanOneServer = servers.length > 1;
useEffect(() => {
const channelMembers = members?.filter((m) => m.userId !== currentUserId);
@ -119,13 +135,24 @@ export const CallNotification = ({
}
}, []);
// We only need to getServerDisplayName once
useEffect(() => {
async function getName() {
setServerName(await getServerDisplayName(incomingCall.serverUrl));
}
if (moreThanOneServer) {
getName();
}
}, [moreThanOneServer, incomingCall.serverUrl]);
const onContainerPress = useCallback(async () => {
if (serverUrl !== incomingCall.serverUrl) {
if (incomingCall.serverUrl !== serverUrl) {
await DatabaseManager.setActiveServerDatabase(incomingCall.serverUrl);
await WebsocketManager.initializeClient(incomingCall.serverUrl);
}
switchToChannelById(incomingCall.serverUrl, incomingCall.channelID);
}, [serverUrl, incomingCall]);
}, [incomingCall, serverUrl]);
const onDismissPress = useCallback(() => {
removeIncomingCall(serverUrl, incomingCall.callID, incomingCall.channelID);
@ -143,7 +170,7 @@ export const CallNotification = ({
name: displayUsername(incomingCall.callerModel, intl.locale, teammateNameDisplay),
}}
style={style.text}
numberOfLines={2}
numberOfLines={1}
ellipsizeMode={'tail'}
/>
);
@ -158,14 +185,14 @@ export const CallNotification = ({
num: (members?.length || 2) - 1,
}}
style={style.text}
numberOfLines={2}
numberOfLines={1}
ellipsizeMode={'tail'}
/>
);
}
return (
<View style={[style.outerContainer]}>
<View style={[style.outerContainer, moreThanOneServer && style.outerContainerServerName]}>
<Pressable
style={[style.innerContainer, onCallsScreen && style.innerOnCallsScreen]}
onPress={onContainerPress}
@ -180,6 +207,11 @@ export const CallNotification = ({
</View>
<View style={style.textContainer}>
{message}
{moreThanOneServer &&
<Text style={style.textServerName}>
{serverName}
</Text>
}
</View>
<Pressable onPress={onDismissPress}>
<View style={style.dismissContainer}>

View file

@ -5,6 +5,7 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeAllActiveServers} from '@app/queries/app/servers';
import {CallNotification} from '@calls/components/call_notification/call_notification';
import DatabaseManager from '@database/manager';
import {observeChannelMembers} from '@queries/servers/channel';
@ -33,6 +34,7 @@ const enhanced = withObservables(['incomingCall'], ({incomingCall}: OwnProps) =>
);
return {
servers: observeAllActiveServers(),
currentUserId,
teammateNameDisplay,
members,

View file

@ -8,13 +8,26 @@ import {useIntl} from 'react-intl';
import {Alert, Platform} from 'react-native';
import Permissions from 'react-native-permissions';
import {CALL_ERROR_BAR_HEIGHT, CALL_NOTIFICATION_BAR_HEIGHT, CURRENT_CALL_BAR_HEIGHT, JOIN_CALL_BAR_HEIGHT} from '@app/constants/view';
import {initializeVoiceTrack} from '@calls/actions/calls';
import {setMicPermissionsGranted, useCallsState, useChannelsWithCalls, useCurrentCall, useGlobalCallsState, useIncomingCalls} from '@calls/state';
import {
setMicPermissionsGranted,
useCallsState,
useChannelsWithCalls,
useCurrentCall,
useGlobalCallsState,
useIncomingCalls,
} from '@calls/state';
import {errorAlert} from '@calls/utils';
import {
CALL_ERROR_BAR_HEIGHT,
CALL_NOTIFICATION_BAR_HEIGHT,
CURRENT_CALL_BAR_HEIGHT,
JOIN_CALL_BAR_HEIGHT,
} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useAppState} from '@hooks/device';
import NetworkManager from '@managers/network_manager';
import {queryAllActiveServers} from '@queries/app/servers';
import {getFullErrorMessage} from '@utils/errors';
import type {Client} from '@client/rest';
@ -110,26 +123,35 @@ export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
}, [appState]);
};
export const useCallsAdjustment = (serverUrl: string, channelId: string) => {
export const useCallsAdjustment = (serverUrl: string, channelId: string): number => {
const incomingCalls = useIncomingCalls().incomingCalls;
const channelsWithCalls = useChannelsWithCalls(serverUrl);
const callsState = useCallsState(serverUrl);
const globalCallsState = useGlobalCallsState();
const currentCall = useCurrentCall();
const [numServers, setNumServers] = useState(1);
const dismissed = Boolean(callsState.calls[channelId]?.dismissed[callsState.myUserId]);
const inCurrentCall = currentCall?.id === channelId;
const joinCallBannerVisible = Boolean(channelsWithCalls[channelId]) && !dismissed && !inCurrentCall;
useEffect(() => {
const getNumServers = async () => {
const query = await queryAllActiveServers()?.fetch();
setNumServers(query?.length || 0);
};
getNumServers();
}, []);
// Do we have calls banners?
const currentCallBarVisible = Boolean(currentCall);
const micPermissionsError = !globalCallsState.micPermissionsGranted && (currentCall && !currentCall.micPermissionsErrorDismissed);
const callQualityAlert = Boolean(currentCall?.callQualityAlert);
const incomingCallsShowing = incomingCalls.filter((ic) => ic.channelID !== channelId);
const callsIncomingAdjustment = (incomingCallsShowing.length * CALL_NOTIFICATION_BAR_HEIGHT) + (incomingCallsShowing.length * 8);
const callsAdjustment = (currentCallBarVisible ? CURRENT_CALL_BAR_HEIGHT + 8 : 0) +
const notificationBarHeight = CALL_NOTIFICATION_BAR_HEIGHT + (numServers > 1 ? 8 : 0);
const callsIncomingAdjustment = (incomingCallsShowing.length * notificationBarHeight) + (incomingCallsShowing.length * 8);
return (currentCallBarVisible ? CURRENT_CALL_BAR_HEIGHT + 8 : 0) +
(micPermissionsError ? CALL_ERROR_BAR_HEIGHT + 8 : 0) +
(callQualityAlert ? CALL_ERROR_BAR_HEIGHT + 8 : 0) +
(joinCallBannerVisible ? JOIN_CALL_BAR_HEIGHT + 8 : 0) +
callsIncomingAdjustment;
return callsAdjustment;
};

View file

@ -32,6 +32,7 @@ type Props = {
canEnableDisableCalls: boolean;
isCallsEnabledInChannel: boolean;
canManageMembers: boolean;
isCRTEnabled: boolean;
canManageSettings: boolean;
isGuestUser: boolean;
isConvertGMFeatureAvailable: boolean;
@ -55,6 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
}));
const ChannelInfo = ({
isCRTEnabled,
channelId,
closeButtonId,
componentId,
@ -113,6 +115,7 @@ const ChannelInfo = ({
type={type}
callsEnabled={callsAvailable}
canManageMembers={canManageMembers}
isCRTEnabled={isCRTEnabled}
canManageSettings={canManageSettings}
/>
<View style={styles.separator}/>

View file

@ -17,6 +17,7 @@ import {
observeCurrentTeamId,
observeCurrentUserId,
} from '@queries/servers/system';
import {observeIsCRTEnabled} from '@queries/servers/thread';
import {observeCurrentUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user';
import {isTypeDMorGM} from '@utils/channel';
import {isMinimumServerVersion} from '@utils/helpers';
@ -126,6 +127,7 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
canEnableDisableCalls,
isCallsEnabledInChannel,
canManageMembers,
isCRTEnabled: observeIsCRTEnabled(database),
canManageSettings,
isGuestUser,
isConvertGMFeatureAvailable,

View file

@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {defineMessage, useIntl} from 'react-intl';
import {updateChannelNotifyProps} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {
CHANNEL_AUTO_FOLLOW_THREADS_FALSE,
CHANNEL_AUTO_FOLLOW_THREADS_TRUE,
} from '@constants/channel';
import {useServerUrl} from '@context/server';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
followedStatus: boolean;
displayName: string;
};
const AutoFollowThreads = ({channelId, displayName, followedStatus}: Props) => {
const [autoFollow, setAutoFollow] = useState(followedStatus);
const serverUrl = useServerUrl();
const intl = useIntl();
const toggleFollow = preventDoubleTap(async () => {
const props: Partial<ChannelNotifyProps> = {
channel_auto_follow_threads: followedStatus ? CHANNEL_AUTO_FOLLOW_THREADS_FALSE : CHANNEL_AUTO_FOLLOW_THREADS_TRUE,
};
setAutoFollow((v) => !v);
const result = await updateChannelNotifyProps(serverUrl, channelId, props);
if (result?.error) {
alertErrorWithFallback(
intl,
result.error,
defineMessage({
id: 'channel_info.channel_auto_follow_threads_failed',
defaultMessage: 'An error occurred trying to auto follow all threads in channel {displayName}',
}),
{displayName},
);
setAutoFollow((v) => !v);
}
});
return (
<OptionItem
action={toggleFollow}
label={intl.formatMessage({id: 'channel_info.channel_auto_follow_threads', defaultMessage: 'Follow all threads in this channel'})}
icon='message-plus-outline'
type='toggle'
selected={autoFollow}
testID={`channel_info.options.channel_auto_follow_threads.option.toggled.${autoFollow}`}
/>
);
};
export default AutoFollowThreads;

View file

@ -0,0 +1,35 @@
// 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 {Channel} from '@constants';
import {observeChannel, observeChannelSettings} from '@queries/servers/channel';
import AutoFollowThreads from './auto_follow_threads';
import type {WithDatabaseArgs} from '@typings/database/database';
type Props = WithDatabaseArgs & {
channelId: string;
}
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
const channel = observeChannel(database, channelId);
const settings = observeChannelSettings(database, channelId);
const followedStatus = settings.pipe(
switchMap((s) => {
return of$(s?.notifyProps?.channel_auto_follow_threads === Channel.CHANNEL_AUTO_FOLLOW_THREADS_TRUE);
}),
);
return {
followedStatus,
displayName: channel.pipe(switchMap((c) => of$(c?.displayName))),
};
});
export default withDatabase(enhanced(AutoFollowThreads));

View file

@ -2,35 +2,49 @@
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {useIntl} from 'react-intl';
import {defineMessage, useIntl} from 'react-intl';
import {updateChannelNotifyProps} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {useServerUrl} from '@context/server';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
ignoring: boolean;
displayName: string;
}
const IgnoreMentions = ({channelId, ignoring}: Props) => {
const IgnoreMentions = ({channelId, ignoring, displayName}: Props) => {
const [ignored, setIgnored] = useState(ignoring);
const serverUrl = useServerUrl();
const {formatMessage} = useIntl();
const intl = useIntl();
const toggleIgnore = preventDoubleTap(() => {
const toggleIgnore = preventDoubleTap(async () => {
const props: Partial<ChannelNotifyProps> = {
ignore_channel_mentions: ignoring ? 'off' : 'on',
};
setIgnored(!ignored);
updateChannelNotifyProps(serverUrl, channelId, props);
setIgnored((v) => !v);
const result = await updateChannelNotifyProps(serverUrl, channelId, props);
if (result?.error) {
alertErrorWithFallback(
intl,
result.error,
defineMessage({
id: 'channel_info.channel_auto_follow_threads_failed',
defaultMessage: 'An error occurred trying to auto follow all threads in channel {displayName}',
}),
{displayName},
);
setIgnored((v) => !v);
}
});
return (
<OptionItem
action={toggleIgnore}
label={formatMessage({id: 'channel_info.ignore_mentions', defaultMessage: 'Ignore @channel, @here, @all'})}
label={intl.formatMessage({id: 'channel_info.ignore_mentions', defaultMessage: 'Ignore @channel, @here, @all'})}
icon='at'
type='toggle'
selected={ignored}

View file

@ -7,7 +7,7 @@ import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
import {Channel} from '@constants';
import {observeChannelSettings} from '@queries/servers/channel';
import {observeChannel, observeChannelSettings} from '@queries/servers/channel';
import {observeCurrentUser} from '@queries/servers/user';
import IgnoreMentions from './ignore_mentions';
@ -34,6 +34,7 @@ const isChannelMentionsIgnored = (channelNotifyProps?: Partial<ChannelNotifyProp
};
const enhanced = withObservables(['channelId'], ({channelId, database}: Props) => {
const channel = observeChannel(database, channelId);
const currentUser = observeCurrentUser(database);
const settings = observeChannelSettings(database, channelId);
const ignoring = currentUser.pipe(
@ -43,6 +44,7 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: Props) =
return {
ignoring,
displayName: channel.pipe(switchMap((c) => of$(c?.displayName))),
};
});

View file

@ -8,6 +8,7 @@ import {General} from '@constants';
import {isTypeDMorGM} from '@utils/channel';
import AddMembers from './add_members';
import AutoFollowThreads from './auto_follow_threads';
import ChannelFiles from './channel_files';
import EditChannel from './edit_channel';
import IgnoreMentions from './ignore_mentions';
@ -20,6 +21,7 @@ type Props = {
type?: ChannelType;
callsEnabled: boolean;
canManageMembers: boolean;
isCRTEnabled: boolean;
canManageSettings: boolean;
}
@ -28,15 +30,21 @@ const Options = ({
type,
callsEnabled,
canManageMembers,
isCRTEnabled,
canManageSettings,
}: Props) => {
const isDMorGM = isTypeDMorGM(type);
return (
<>
{type !== General.DM_CHANNEL &&
<IgnoreMentions channelId={channelId}/>
}
{type !== General.DM_CHANNEL && (
<>
{isCRTEnabled && (
<AutoFollowThreads channelId={channelId}/>
)}
<IgnoreMentions channelId={channelId}/>
</>
)}
<NotificationPreference channelId={channelId}/>
<PinnedMessages channelId={channelId}/>
<ChannelFiles channelId={channelId}/>

View file

@ -17,6 +17,7 @@ import AppVersion from '@components/app_version';
import {Screens, Launch} from '@constants';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import {getServerCredentials} from '@init/credentials';
import PushNotifications from '@init/push_notifications';
import NetworkManager from '@managers/network_manager';
import {getServerByDisplayName, getServerByIdentifier} from '@queries/app/servers';
@ -244,7 +245,8 @@ const Server = ({
}
const server = await getServerByDisplayName(displayName);
if (server && server.lastActiveAt > 0) {
const credentials = await getServerCredentials(serverUrl);
if (server && server.lastActiveAt > 0 && credentials?.token) {
setButtonDisabled(true);
setDisplayNameError(formatMessage({
id: 'mobile.server_name.exists',
@ -330,9 +332,10 @@ const Server = ({
}
const server = await getServerByIdentifier(data.config.DiagnosticId);
const credentials = await getServerCredentials(serverUrl);
setConnecting(false);
if (server && server.lastActiveAt > 0) {
if (server && server.lastActiveAt > 0 && credentials?.token) {
setButtonDisabled(true);
setUrlError(formatMessage({
id: 'mobile.server_identifier.exists',

View file

@ -112,6 +112,8 @@
"channel_info.archive_description.cannot_view_archived": "This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.\n\nAre you sure you wish to archive the {term} {name}?",
"channel_info.archive_failed": "An error occurred trying to archive the channel {displayName}",
"channel_info.archive_title": "Archive {term}",
"channel_info.channel_auto_follow_threads": "Follow all threads in this channel",
"channel_info.channel_auto_follow_threads_failed": "An error occurred trying to auto follow all threads in channel {displayName}",
"channel_info.channel_files": "Files",
"channel_info.close": "Close",
"channel_info.close_dm": "Close direct message",

12
package-lock.json generated
View file

@ -165,7 +165,7 @@
"jest-cli": "29.6.2",
"jetifier": "2.0.0",
"metro-react-native-babel-preset": "0.77.0",
"mmjstool": "github:mattermost/mattermost-utilities#e65ab00f22628cbdee736fd2e4f192f07225e82d",
"mmjstool": "github:mattermost/mattermost-utilities#83b1b311972b8f5e750aae4019457a40abb5aa44",
"mock-async-storage": "2.2.0",
"nock": "13.3.2",
"patch-package": "8.0.0",
@ -17464,8 +17464,8 @@
},
"node_modules/mmjstool": {
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/mattermost/mattermost-utilities.git#e65ab00f22628cbdee736fd2e4f192f07225e82d",
"integrity": "sha512-tY5XH26LwPztivGSbUWWaxZCkp/cKWTrtWBUnG8L1J+QIcLua8hzyZBe+VGDGdoHUJn4PbGpawJlUyoiC3r9Lg==",
"resolved": "git+ssh://git@github.com/mattermost/mattermost-utilities.git#83b1b311972b8f5e750aae4019457a40abb5aa44",
"integrity": "sha512-SFAbT+eN1mvgSfRTe8k6IMKVWbhGItTJtxZ+Pt0mX+fe8pakB3MkIkWzHBHVnEDI9Ek9kmQFGF1aZwzKFHXyYQ==",
"dev": true,
"dependencies": {
"estree-walk": "^2.2.0",
@ -35875,10 +35875,10 @@
}
},
"mmjstool": {
"version": "git+ssh://git@github.com/mattermost/mattermost-utilities.git#e65ab00f22628cbdee736fd2e4f192f07225e82d",
"integrity": "sha512-tY5XH26LwPztivGSbUWWaxZCkp/cKWTrtWBUnG8L1J+QIcLua8hzyZBe+VGDGdoHUJn4PbGpawJlUyoiC3r9Lg==",
"version": "git+ssh://git@github.com/mattermost/mattermost-utilities.git#83b1b311972b8f5e750aae4019457a40abb5aa44",
"integrity": "sha512-SFAbT+eN1mvgSfRTe8k6IMKVWbhGItTJtxZ+Pt0mX+fe8pakB3MkIkWzHBHVnEDI9Ek9kmQFGF1aZwzKFHXyYQ==",
"dev": true,
"from": "mmjstool@github:mattermost/mattermost-utilities#e65ab00f22628cbdee736fd2e4f192f07225e82d",
"from": "mmjstool@github:mattermost/mattermost-utilities#83b1b311972b8f5e750aae4019457a40abb5aa44",
"requires": {
"estree-walk": "^2.2.0",
"filehound": "^1.17.5",

View file

@ -166,7 +166,7 @@
"jest-cli": "29.6.2",
"jetifier": "2.0.0",
"metro-react-native-babel-preset": "0.77.0",
"mmjstool": "github:mattermost/mattermost-utilities#e65ab00f22628cbdee736fd2e4f192f07225e82d",
"mmjstool": "github:mattermost/mattermost-utilities#83b1b311972b8f5e750aae4019457a40abb5aa44",
"mock-async-storage": "2.2.0",
"nock": "13.3.2",
"patch-package": "8.0.0",

View file

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