diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index ed7aee28be..34405c5ca4 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -11,7 +11,10 @@ import DatabaseManager from '@database/manager'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; import {extractChannelDisplayName} from '@helpers/database'; import PushNotifications from '@init/push_notifications'; -import {prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel, getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes} from '@queries/servers/channel'; +import { + prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel, + getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes, +} from '@queries/servers/channel'; import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system'; import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team'; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index b8ad866923..b125df807f 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -13,13 +13,14 @@ import DatabaseManager from '@database/manager'; import {privateChannelJoinPrompt} from '@helpers/api/channel'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; import NetworkManager from '@managers/network_manager'; -import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo} from '@queries/servers/channel'; +import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds} from '@queries/servers/channel'; import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system'; import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel'; +import {showMuteChannelSnackbar} from '@utils/snack_bar'; import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; import {displayGroupMessageName, displayUsername} from '@utils/user'; @@ -1055,3 +1056,64 @@ export async function searchAllChannels(serverUrl: string, term: string, archive return {error}; } } + +export const updateChannelNotifyProps = async (serverUrl: string, channelId: string, props: Partial) => { + let client: Client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + const userId = await getCurrentUserId(database); + const notifyProps = {...props, channel_id: channelId, user_id: userId} as ChannelNotifyProps & {channel_id: string; user_id: string}; + + await client.updateChannelNotifyProps(notifyProps); + + return { + notifyProps, + }; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + return {error}; + } +}; + +export const toggleMuteChannel = async (serverUrl: string, channelId: string, showSnackBar = false) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + + try { + const channelSettings = await queryMyChannelSettingsByIds(database, [channelId]).fetch(); + const myChannelSetting = channelSettings?.[0]; + const mark_unread = myChannelSetting.notifyProps?.mark_unread === 'mention' ? 'all' : 'mention'; + + const notifyProps: Partial = {...myChannelSetting.notifyProps, mark_unread}; + await updateChannelNotifyProps(serverUrl, channelId, notifyProps); + + await database.write(async () => { + await myChannelSetting.update((c) => { + c.notifyProps = notifyProps; + }); + }); + + if (showSnackBar) { + const onUndo = () => toggleMuteChannel(serverUrl, channelId, false); + showMuteChannelSnackbar(onUndo); + } + + return { + notifyProps, + }; + } catch (error) { + return {error}; + } +}; diff --git a/app/constants/snack_bar.ts b/app/constants/snack_bar.ts index 484c4162fc..3638f486ca 100644 --- a/app/constants/snack_bar.ts +++ b/app/constants/snack_bar.ts @@ -7,9 +7,7 @@ import keyMirror from '@utils/key_mirror'; export const SNACK_BAR_TYPE = keyMirror({ LINK_COPIED: null, MESSAGE_COPIED: null, - FOLLOW_THREAD: null, MUTE_CHANNEL: null, - FAILED_TO_SAVE_MESSAGE: null, }); type SnackBarConfig = { @@ -17,7 +15,7 @@ type SnackBarConfig = { defaultMessage: string; iconName: string; canUndo: boolean; -} +}; export const SNACK_BAR_CONFIG: Record = { LINK_COPIED: { id: t('snack.bar.link.copied'), @@ -31,12 +29,6 @@ export const SNACK_BAR_CONFIG: Record = { iconName: 'content-copy', canUndo: false, }, - FOLLOW_THREAD: { - id: t('snack.bar.follow.thread'), - defaultMessage: 'You\'re now following this thread', - iconName: 'message-check-outline', - canUndo: true, - }, MUTE_CHANNEL: { id: t('snack.bar.mute.channel'), defaultMessage: 'This channel was muted', diff --git a/app/screens/snack_bar/index.tsx b/app/screens/snack_bar/index.tsx index 3787414514..526a8727a9 100644 --- a/app/screens/snack_bar/index.tsx +++ b/app/screens/snack_bar/index.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; import {DeviceEventEmitter, Text, TouchableOpacity, useWindowDimensions, ViewStyle} from 'react-native'; import {Gesture, GestureDetector, GestureHandlerRootView} from 'react-native-gesture-handler'; @@ -71,12 +71,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { type SnackBarProps = { componentId: string; - onUndoPress?: () => void; + onAction?: () => void; barType: keyof typeof SNACK_BAR_TYPE; sourceScreen: typeof Screens[keyof typeof Screens]; } -const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarProps) => { +const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps) => { const [showSnackBar, setShowSnackBar] = useState(); const intl = useIntl(); const theme = useTheme(); @@ -85,15 +85,12 @@ const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarPro const offset = useSharedValue(0); const isPanned = useSharedValue(false); const baseTimer = useRef(); + const mounted = useRef(false); + const userHasUndo = useRef(false); const config = SNACK_BAR_CONFIG[barType]; const styles = getStyleSheet(theme); - const onPressHandler = useCallback(() => { - dismissOverlay(componentId); - onUndoPress?.(); - }, [onUndoPress, componentId]); - const snackBarStyle = useMemo(() => { const diffWidth = windowWidth - TABLET_SIDEBAR_WIDTH; @@ -147,11 +144,13 @@ const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarPro }, [offset.value, isPanned.value]); const hideSnackBar = () => { - setShowSnackBar(false); + if (mounted?.current) { + setShowSnackBar(false); + } }; const stopTimers = () => { - if (baseTimer.current) { + if (baseTimer?.current) { clearTimeout(baseTimer.current); } }; @@ -174,8 +173,14 @@ const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarPro offset.value = withTiming(200, {duration}, () => runOnJS(hideSnackBar)()); }; + const onUndoPressHandler = () => { + userHasUndo.current = true; + animateHiding(false); + }; + // This effect hides the snack bar after 3 seconds useEffect(() => { + mounted.current = true; baseTimer.current = setTimeout(() => { if (!isPanned.value) { animateHiding(false); @@ -183,18 +188,20 @@ const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarPro }, 3000); return () => { - if (baseTimer.current) { - clearTimeout(baseTimer.current); - } + stopTimers(); + mounted.current = false; }; }, [isPanned.value]); // This effect dismisses the Navigation Overlay after we have hidden the snack bar useEffect(() => { if (showSnackBar === false) { + if (userHasUndo?.current) { + onAction?.(); + } dismissOverlay(componentId); } - }, [showSnackBar]); + }, [showSnackBar, onAction]); // This effect checks if we are navigating away and if so, it dismisses the snack bar useEffect(() => { @@ -226,8 +233,8 @@ const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarPro textStyle={styles.text} style={styles.toast} > - {config.canUndo && onUndoPress && ( - + {config.canUndo && onAction && ( + {intl.formatMessage({ id: 'snack.bar.undo', diff --git a/app/utils/channel/index.ts b/app/utils/channel/index.ts index e12ee36c73..06e8a4b141 100644 --- a/app/utils/channel/index.ts +++ b/app/utils/channel/index.ts @@ -118,3 +118,17 @@ export function generateChannelNameFromDisplayName(displayName: string) { } return name; } + +export function compareNotifyProps(propsA: Partial, propsB: Partial): boolean { + if ( + propsA.desktop !== propsB.desktop || + propsA.email !== propsB.email || + propsA.mark_unread !== propsB.mark_unread || + propsA.push !== propsB.push || + propsA.ignore_channel_mentions !== propsB.ignore_channel_mentions + ) { + return false; + } + + return true; +} diff --git a/app/utils/snack_bar/index.ts b/app/utils/snack_bar/index.ts index a99fdaed3b..0d80b4e350 100644 --- a/app/utils/snack_bar/index.ts +++ b/app/utils/snack_bar/index.ts @@ -6,7 +6,7 @@ import {showOverlay} from '@screens/navigation'; type ShowSnackBarArgs = { barType: keyof typeof SNACK_BAR_TYPE; - onPress?: () => void; + onAction?: () => void; sourceScreen?: typeof Screens[keyof typeof Screens]; }; @@ -14,3 +14,10 @@ export const showSnackBar = (passProps: ShowSnackBarArgs) => { const screen = Screens.SNACK_BAR; showOverlay(screen, passProps); }; + +export const showMuteChannelSnackbar = (onAction: () => void) => { + return showSnackBar({ + onAction, + barType: SNACK_BAR_TYPE.MUTE_CHANNEL, + }); +}; diff --git a/types/api/channels.d.ts b/types/api/channels.d.ts index 5122c512b4..0cd1e4f1e3 100644 --- a/types/api/channels.d.ts +++ b/types/api/channels.d.ts @@ -8,6 +8,7 @@ type ChannelStats = { guest_count: number; pinnedpost_count: number; }; + type ChannelNotifyProps = { desktop: 'default' | 'all' | 'mention' | 'none'; email: 'default' | 'all' | 'mention' | 'none';