diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 43da25285..00a02a11c 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -100,6 +100,7 @@ export function buildApiChatFromDialog( unreadMentionsCount, unreadReactionsCount, isMuted, + muteUntil, ...(unreadMark && { hasUnreadMark: true }), ...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }), ...buildApiChatFieldsFromPeerEntity(peerEntity), @@ -547,8 +548,8 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und unreadMentionsCount, unreadReactionsCount, fromId: getApiChatIdFromMtpPeer(fromId), - // TODO[forums] `muteUntil` should not really be parsed here - isMuted: silent || (muteUntil !== undefined ? muteUntil > 0 : undefined), + isMuted: silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil), + muteUntil, }; } diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index bab19dca1..1bd108fba 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -136,6 +136,7 @@ export function buildApiNotifyException( isMuted: silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil), ...(!hasSound && { isSilent: true }), ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), + muteUntil, }; } @@ -154,6 +155,7 @@ export function buildApiNotifyExceptionTopic( isMuted: silent || (typeof muteUntil === 'number' && getServerTime() < muteUntil), ...(!hasSound && { isSilent: true }), ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), + muteUntil, }; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index f42e07c9a..217c0de03 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -40,7 +40,8 @@ import { buildApiChatSettings, buildApiChatReactions, buildApiTopic, - buildApiChatlistInvite, buildApiChatlistExportedInvite, + buildApiChatlistInvite, + buildApiChatlistExportedInvite, } from '../apiBuilders/chats'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users'; @@ -58,12 +59,17 @@ import { generateRandomBigInt, } from '../gramjsBuilders'; import { - addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb, isChatFolder, + addEntitiesWithPhotosToLocalDb, + addMessageToLocalDb, + addPhotoToLocalDb, + isChatFolder, + } from '../helpers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPhoto } from '../apiBuilders/common'; import { buildStickerSet } from '../apiBuilders/symbols'; import localDb from '../localDb'; +import { scheduleMutedChatUpdate } from '../scheduleUnmute'; type FullChatData = { fullInfo: ApiChatFullInfo; @@ -161,6 +167,8 @@ export async function fetchChats({ chats.push(chat); + scheduleMutedChatUpdate(chat.id, chat.muteUntil, onUpdate); + if (withPinned && dialog.pinned) { orderedPinnedIds.push(chat.id); } @@ -330,14 +338,18 @@ export async function requestChatUpdate({ ? lastLocalMessage : lastRemoteMessage; + const chatUpdate = { + ...buildApiChatFromDialog(dialog, peerEntity), + ...(!noLastMessage && { lastMessage }), + }; + onUpdate({ '@type': 'updateChat', id, - chat: { - ...buildApiChatFromDialog(dialog, peerEntity), - ...(!noLastMessage && { lastMessage }), - }, + chat: chatUpdate, }); + + scheduleMutedChatUpdate(chatUpdate.id, chatUpdate.muteUntil, onUpdate); } export function saveDraft({ @@ -558,15 +570,18 @@ async function getFullChannelInfo( } export async function updateChatMutedState({ - chat, isMuted, + chat, isMuted, muteUntil = 0, }: { - chat: ApiChat; isMuted: boolean; + chat: ApiChat; isMuted: boolean; muteUntil?: number; }) { + if (isMuted && !muteUntil) { + muteUntil = MAX_INT_32; + } await invokeRequest(new GramJs.account.UpdateNotifySettings({ peer: new GramJs.InputNotifyPeer({ peer: buildInputPeer(chat.id, chat.accessHash), }), - settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : 0 }), + settings: new GramJs.InputPeerNotifySettings({ muteUntil }), })); onUpdate({ @@ -582,16 +597,19 @@ export async function updateChatMutedState({ } export async function updateTopicMutedState({ - chat, topicId, isMuted, + chat, topicId, isMuted, muteUntil = 0, }: { - chat: ApiChat; topicId: number; isMuted: boolean; + chat: ApiChat; topicId: number; isMuted: boolean; muteUntil?: number; }) { + if (isMuted && !muteUntil) { + muteUntil = MAX_INT_32; + } await invokeRequest(new GramJs.account.UpdateNotifySettings({ peer: new GramJs.InputNotifyForumTopic({ peer: buildInputPeer(chat.id, chat.accessHash), topMsgId: topicId, }), - settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : 0 }), + settings: new GramJs.InputPeerNotifySettings({ muteUntil }), })); onUpdate({ diff --git a/src/api/gramjs/scheduleUnmute.ts b/src/api/gramjs/scheduleUnmute.ts new file mode 100644 index 000000000..48a5c11ce --- /dev/null +++ b/src/api/gramjs/scheduleUnmute.ts @@ -0,0 +1,51 @@ +import type { OnApiUpdate } from '../types'; +import { getServerTime } from '../../util/serverTime'; +import { MAX_INT_32 } from '../../config'; + +type UnmuteQueueItem = { chatId: string; topicId?: number; muteUntil: number }; +const unmuteTimers = new Map(); +const unmuteQueue: Array = []; +const scheduleUnmute = (item: UnmuteQueueItem, onUpdate: NoneToVoidFunction) => { + const id = item.topicId ? `${item.chatId}-${item.topicId}` : item.chatId; + if (unmuteTimers.has(id)) { + clearTimeout(unmuteTimers.get(id)); + unmuteTimers.delete(id); + } + if (item.muteUntil === MAX_INT_32 || item.muteUntil <= getServerTime()) return; + unmuteQueue.push(item); + unmuteQueue.sort((a, b) => b.muteUntil - a.muteUntil); + const next = unmuteQueue.pop(); + if (!next) return; + const timer = setTimeout(() => { + onUpdate(); + if (unmuteQueue.length) { + const afterNext = unmuteQueue.pop(); + if (afterNext) scheduleUnmute(afterNext, onUpdate); + } + }, (item.muteUntil - getServerTime()) * 1000); + unmuteTimers.set(id, timer); +}; + +export function scheduleMutedChatUpdate(chatId: string, muteUntil = 0, onUpdate: OnApiUpdate) { + scheduleUnmute({ + chatId, + muteUntil, + }, () => onUpdate({ + '@type': 'updateNotifyExceptions', + chatId, + isMuted: false, + })); +} + +export function scheduleMutedTopicUpdate(chatId: string, topicId: number, muteUntil = 0, onUpdate: OnApiUpdate) { + scheduleUnmute({ + chatId, + topicId, + muteUntil, + }, () => onUpdate({ + '@type': 'updateTopicNotifyExceptions', + chatId, + topicId, + isMuted: false, + })); +} diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index bf03be74e..56bbc5df4 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -66,6 +66,7 @@ import { import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols'; import { buildApiBotMenuButton } from './apiBuilders/bots'; +import { scheduleMutedTopicUpdate, scheduleMutedChatUpdate } from './scheduleUnmute'; type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } @@ -635,19 +636,23 @@ export function updater(update: Update) { update instanceof GramJs.UpdateNotifySettings && update.peer instanceof GramJs.NotifyPeer ) { + const payload = buildApiNotifyException(update.notifySettings, update.peer.peer); + scheduleMutedChatUpdate(payload.chatId, payload.muteUntil, onUpdate); onUpdate({ '@type': 'updateNotifyExceptions', - ...buildApiNotifyException(update.notifySettings, update.peer.peer), + ...payload, }); } else if ( update instanceof GramJs.UpdateNotifySettings && update.peer instanceof GramJs.NotifyForumTopic ) { + const payload = buildApiNotifyExceptionTopic( + update.notifySettings, update.peer.peer, update.peer.topMsgId, + ); + scheduleMutedTopicUpdate(payload.chatId, payload.topicId, payload.muteUntil, onUpdate); onUpdate({ '@type': 'updateTopicNotifyExceptions', - ...buildApiNotifyExceptionTopic( - update.notifySettings, update.peer.peer, update.peer.topMsgId, - ), + ...payload, }); } else if ( update instanceof GramJs.UpdateUserTyping diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 7ea1c8253..7cc984caa 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -25,6 +25,7 @@ export interface ApiChat { unreadReactionsCount?: number; isVerified?: boolean; isMuted?: boolean; + muteUntil?: number; isSignaturesShown?: boolean; hasPrivateLink?: boolean; accessHash?: string; @@ -221,8 +222,8 @@ export interface ApiTopic { unreadMentionsCount: number; unreadReactionsCount: number; fromId: string; - isMuted?: boolean; + muteUntil?: number; } export interface ApiChatlistInviteNew { diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index cc1d468e7..c4a1bafcf 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -39,6 +39,7 @@ export { default as NewChatStep1 } from '../components/left/newChat/NewChatStep1 export { default as NewChatStep2 } from '../components/left/newChat/NewChatStep2'; export { default as ArchivedChats } from '../components/left/ArchivedChats'; export { default as ChatFolderModal } from '../components/left/ChatFolderModal'; +export { default as MuteChatModal } from '../components/left/MuteChatModal'; export { default as ContextMenuContainer } from '../components/middle/message/ContextMenuContainer'; export { default as SponsoredMessageContextMenuContainer } diff --git a/src/components/left/MuteChatModal.async.tsx b/src/components/left/MuteChatModal.async.tsx new file mode 100644 index 000000000..c6d3a8d95 --- /dev/null +++ b/src/components/left/MuteChatModal.async.tsx @@ -0,0 +1,16 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; +import type { OwnProps } from './MuteChatModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const MuteChatModalAsync: FC = (props) => { + const { isOpen } = props; + const MuteChatModal = useModuleLoader(Bundles.Extra, 'MuteChatModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return MuteChatModal ? : undefined; +}; + +export default memo(MuteChatModalAsync); diff --git a/src/components/left/MuteChatModal.tsx b/src/components/left/MuteChatModal.tsx new file mode 100644 index 000000000..d9f065667 --- /dev/null +++ b/src/components/left/MuteChatModal.tsx @@ -0,0 +1,92 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { + useCallback, memo, useMemo, useState, +} from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import { MAX_INT_32 } from '../../config'; +import useLang from '../../hooks/useLang'; + +import Modal from '../ui/Modal'; +import Button from '../ui/Button'; +import RadioGroup from '../ui/RadioGroup'; + +export type OwnProps = { + isOpen: boolean; + chatId: string; + topicId?: number; + onClose: () => void; + onCloseAnimationEnd?: () => void; +}; + +enum MuteDuration { + OneHour = '3600', + FourHours = '14400', + EightHours = '28800', + OneDay = '86400', + ThreeDays = '259200', + Forever = '-1', +} + +const MuteChatModal: FC = ({ + isOpen, + chatId, + topicId, + onClose, + onCloseAnimationEnd, +}) => { + const [muteUntilOption, setMuteUntilOption] = useState(MuteDuration.Forever); + const { updateChatMutedState, updateTopicMutedState } = getActions(); + + const lang = useLang(); + + const muteForOptions = useMemo(() => [ + { label: lang('MuteFor.Hours', 1), value: MuteDuration.OneHour }, + { label: lang('MuteFor.Hours', 4), value: MuteDuration.FourHours }, + { label: lang('MuteFor.Hours', 8), value: MuteDuration.EightHours }, + { label: lang('MuteFor.Days', 1), value: MuteDuration.OneDay }, + { label: lang('MuteFor.Days', 3), value: MuteDuration.ThreeDays }, + { label: lang('MuteFor.Forever'), value: MuteDuration.Forever }, + ], [lang]); + + const handleSubmit = useCallback(() => { + let muteUntil: number; + if (muteUntilOption === MuteDuration.Forever) { + muteUntil = MAX_INT_32; + } else { + muteUntil = Math.floor(Date.now() / 1000) + Number(muteUntilOption); + } + if (topicId) { + updateTopicMutedState({ chatId, topicId, muteUntil }); + } else { + updateChatMutedState({ chatId, muteUntil }); + } + onClose(); + }, [chatId, muteUntilOption, onClose, topicId]); + + return ( + + +
+ + +
+
+ ); +}; + +export default memo(MuteChatModal); diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index 03776fda7..e3196d471 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -54,6 +54,7 @@ import DeleteChatModal from '../../common/DeleteChatModal'; import ReportModal from '../../common/ReportModal'; import FullNameTitle from '../../common/FullNameTitle'; import ChatFolderModal from '../ChatFolderModal.async'; +import MuteChatModal from '../MuteChatModal.async'; import ChatCallStatus from './ChatCallStatus'; import ChatBadge from './ChatBadge'; import AvatarBadge from './AvatarBadge'; @@ -130,9 +131,11 @@ const Chat: FC = ({ const { isMobile } = useAppLayout(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); + const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag(); const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag(); const [isReportModalOpen, openReportModal, closeReportModal] = useFlag(); const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag(); + const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag(); @@ -183,6 +186,11 @@ const Chat: FC = ({ openDeleteModal(); }, [markRenderDeleteModal, openDeleteModal]); + const handleMute = useCallback(() => { + markRenderMuteModal(); + openMuteModal(); + }, [markRenderMuteModal, openMuteModal]); + const handleChatFolderChange = useCallback(() => { markRenderChatFolderModal(); openChatFolderModal(); @@ -197,6 +205,7 @@ const Chat: FC = ({ chat, user, handleDelete, + handleMute, handleChatFolderChange, handleReport, folderId, @@ -281,6 +290,14 @@ const Chat: FC = ({ chat={chat} /> )} + {shouldRenderMuteModal && ( + + )} {shouldRenderChatFolderModal && ( = ({ const lang = useLang(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); + const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag(); const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag(); + const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const { isPinned, isClosed, @@ -111,6 +114,11 @@ const Topic: FC = ({ deleteTopic({ chatId: chat.id, topicId: topic.id }); }, [chat.id, deleteTopic, topic.id]); + const handleMute = useCallback(() => { + markRenderMuteModal(); + openMuteModal(); + }, [markRenderMuteModal, openMuteModal]); + const { renderSubtitle, ref } = useChatListEntry({ chat, chatId, @@ -138,7 +146,14 @@ const Topic: FC = ({ } }, [openChat, chatId, topic.id, canScrollDown, focusLastMessage]); - const contextActions = useTopicContextActions(topic, chat, wasTopicOpened, canDelete, handleOpenDeleteModal); + const contextActions = useTopicContextActions({ + topic, + chat, + wasOpened: wasTopicOpened, + canDelete, + handleDelete: handleOpenDeleteModal, + handleMute, + }); return ( = ({ /> - {shouldRenderDeleteModal && ( = ({ confirmLabel={lang('Delete')} /> )} + {shouldRenderMuteModal && ( + + )} ); }; diff --git a/src/components/left/main/hooks/useTopicContextActions.ts b/src/components/left/main/hooks/useTopicContextActions.ts index 21b71fd2c..887aac9c8 100644 --- a/src/components/left/main/hooks/useTopicContextActions.ts +++ b/src/components/left/main/hooks/useTopicContextActions.ts @@ -10,13 +10,21 @@ import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/windowEnvironment import useLang from '../../../../hooks/useLang'; -export default function useTopicContextActions( - topic: ApiTopic, - chat: ApiChat, - wasOpened?: boolean, - canDelete?: boolean, - handleDelete?: NoneToVoidFunction, -) { +export default function useTopicContextActions({ + topic, + chat, + wasOpened, + canDelete, + handleDelete, + handleMute, +}: { + topic: ApiTopic; + chat: ApiChat; + wasOpened?: boolean; + canDelete?: boolean; + handleDelete?: NoneToVoidFunction; + handleMute?: NoneToVoidFunction; +}) { const lang = useLang(); return useMemo(() => { @@ -76,9 +84,9 @@ export default function useTopicContextActions( handler: () => updateTopicMutedState({ chatId, topicId, isMuted: false }), } : { - title: lang('ChatList.Mute'), + title: `${lang('ChatList.Mute')}...`, icon: 'mute', - handler: () => updateTopicMutedState({ chatId, topicId, isMuted: true }), + handler: handleMute, }; const actionCloseTopic = canToggleClosed ? (isClosed @@ -109,5 +117,5 @@ export default function useTopicContextActions( actionCloseTopic, actionDelete, ]) as MenuItemContextAction[]; - }, [topic, chat, wasOpened, lang, canDelete, handleDelete]); + }, [topic, chat, wasOpened, lang, canDelete, handleDelete, handleMute]); } diff --git a/src/components/left/search/LeftSearchResultChat.tsx b/src/components/left/search/LeftSearchResultChat.tsx index 63499f6d5..52c654d28 100644 --- a/src/components/left/search/LeftSearchResultChat.tsx +++ b/src/components/left/search/LeftSearchResultChat.tsx @@ -14,9 +14,9 @@ import useSelectWithEnter from '../../../hooks/useSelectWithEnter'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import GroupChatInfo from '../../common/GroupChatInfo'; -import DeleteChatModal from '../../common/DeleteChatModal'; import ListItem from '../../ui/ListItem'; import ChatFolderModal from '../ChatFolderModal.async'; +import MuteChatModal from '../MuteChatModal.async'; type OwnProps = { chatId: string; @@ -42,8 +42,20 @@ const LeftSearchResultChat: FC = ({ isMuted, canChangeFolder, }) => { - const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); + const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag(); const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag(); + const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); + const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); + + const handleChatFolderChange = useCallback(() => { + markRenderChatFolderModal(); + openChatFolderModal(); + }, [markRenderChatFolderModal, openChatFolderModal]); + + const handleMute = useCallback(() => { + markRenderMuteModal(); + openMuteModal(); + }, [markRenderMuteModal, openMuteModal]); const contextActions = useChatContextActions({ chat, @@ -51,8 +63,8 @@ const LeftSearchResultChat: FC = ({ isPinned, isMuted, canChangeFolder, - handleDelete: openDeleteModal, - handleChatFolderChange: openChatFolderModal, + handleMute, + handleChatFolderChange, }, true); const handleClick = useCallback(() => { @@ -77,16 +89,22 @@ const LeftSearchResultChat: FC = ({ ) : ( )} - - + {shouldRenderMuteModal && ( + + )} + {shouldRenderChatFolderModal && ( + + )} ); }; diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 2dd9d625c..68fa410d0 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -36,6 +36,7 @@ import { import useShowTransition from '../../hooks/useShowTransition'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import useLang from '../../hooks/useLang'; +import useFlag from '../../hooks/useFlag'; import useAppLayout from '../../hooks/useAppLayout'; import Portal from '../ui/Portal'; @@ -43,6 +44,7 @@ import Menu from '../ui/Menu'; import MenuItem from '../ui/MenuItem'; import MenuSeparator from '../ui/MenuSeparator'; import DeleteChatModal from '../common/DeleteChatModal'; +import MuteChatModal from '../left/MuteChatModal.async'; import ReportModal from '../common/ReportModal'; import './HeaderMenuContainer.scss'; @@ -170,6 +172,8 @@ const HeaderMenuContainer: FC = ({ const [isMenuOpen, setIsMenuOpen] = useState(true); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isReportModalOpen, setIsReportModalOpen] = useState(false); + const [isMuteModalOpen, setIsMuteModalOpen] = useState(false); + const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag(); const { x, y } = anchor; useShowTransition(isOpen, onCloseAnimationEnd, undefined, false); @@ -187,6 +191,11 @@ const HeaderMenuContainer: FC = ({ onClose(); }, [onClose]); + const closeMuteModal = useCallback(() => { + setIsMuteModalOpen(false); + onClose(); + }, [onClose]); + const handleDelete = useCallback(() => { setIsMenuOpen(false); setIsDeleteModalOpen(true); @@ -215,10 +224,16 @@ const HeaderMenuContainer: FC = ({ restartBot({ chatId }); }, [chatId, restartBot]); - const handleToggleMuteClick = useCallback(() => { - updateChatMutedState({ chatId, isMuted: !isMuted }); + const handleUnmuteClick = useCallback(() => { + updateChatMutedState({ chatId, isMuted: false }); closeMenu(); - }, [chatId, closeMenu, isMuted, updateChatMutedState]); + }, [chatId, closeMenu, updateChatMutedState]); + + const handleMuteClick = useCallback(() => { + markRenderMuteModal(); + setIsMuteModalOpen(true); + setIsMenuOpen(false); + }, []); const handleCreateTopicClick = useCallback(() => { openCreateTopicPanel({ chatId }); @@ -446,13 +461,22 @@ const HeaderMenuContainer: FC = ({ {lang('VideoCall')} )} - {canMute && ( + {canMute && (isMuted ? ( - {lang(isMuted ? 'ChatsUnmute' : 'ChatsMute')} + {lang('ChatsUnmute')} + ) + : ( + + {lang('ChatsMute')}... + + ) )} {(canEnterVoiceChat || canCreateVoiceChat) && ( = ({ chat={chat} /> )} + {canMute && shouldRenderMuteModal && chat?.id && ( + + )} {canReportChat && chat?.id && ( { - const { chatId, isMuted } = payload; + const { chatId, muteUntil = 0 } = payload; const chat = selectChat(global, chatId); if (!chat) { return; } + const isMuted = payload.isMuted ?? muteUntil > 0; + global = updateChat(global, chatId, { isMuted }); setGlobal(global); - void callApi('updateChatMutedState', { chat, isMuted }); + void callApi('updateChatMutedState', { chat, isMuted, muteUntil }); }); addActionHandler('updateTopicMutedState', (global, actions, payload): ActionReturnType => { - const { chatId, isMuted, topicId } = payload; + const { chatId, topicId, muteUntil = 0 } = payload; const chat = selectChat(global, chatId); if (!chat) { return; } + const isMuted = payload.isMuted ?? muteUntil > 0; + global = updateTopic(global, chatId, topicId, { isMuted }); setGlobal(global); void callApi('updateTopicMutedState', { - chat, topicId, isMuted, + chat, topicId, isMuted, muteUntil, }); }); diff --git a/src/global/types.ts b/src/global/types.ts index 36171c976..4995ec1f8 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1624,7 +1624,8 @@ export interface ActionPayloads { }; updateChatMutedState: { chatId: string; - isMuted: boolean; + isMuted?: boolean; + muteUntil?: number; }; updateChat: { @@ -2475,7 +2476,8 @@ export interface ActionPayloads { updateTopicMutedState: { chatId: string; topicId: number; - isMuted: boolean; + isMuted?: boolean; + muteUntil?: number; }; openCreateTopicPanel: { diff --git a/src/hooks/useChatContextActions.ts b/src/hooks/useChatContextActions.ts index 800b11dae..4cf6ec406 100644 --- a/src/hooks/useChatContextActions.ts +++ b/src/hooks/useChatContextActions.ts @@ -20,6 +20,7 @@ const useChatContextActions = ({ isMuted, canChangeFolder, handleDelete, + handleMute, handleChatFolderChange, handleReport, }: { @@ -29,9 +30,10 @@ const useChatContextActions = ({ isPinned?: boolean; isMuted?: boolean; canChangeFolder?: boolean; - handleDelete: () => void; - handleChatFolderChange: () => void; - handleReport?: () => void; + handleDelete?: NoneToVoidFunction; + handleMute?: NoneToVoidFunction; + handleChatFolderChange: NoneToVoidFunction; + handleReport?: NoneToVoidFunction; }, isInSearch = false) => { const lang = useLang(); @@ -73,8 +75,20 @@ const useChatContextActions = ({ } : { title: lang('PinToTop'), icon: 'pin', handler: () => toggleChatPinned({ id: chat.id, folderId: folderId! }) }; + const actionMute = isMuted + ? { + title: lang('ChatList.Unmute'), + icon: 'unmute', + handler: () => updateChatMutedState({ chatId: chat.id, isMuted: false }), + } + : { + title: `${lang('ChatList.Mute')}...`, + icon: 'mute', + handler: handleMute, + }; + if (isInSearch) { - return compact([actionOpenInNewTab, actionPin, actionAddToFolder]); + return compact([actionOpenInNewTab, actionPin, actionAddToFolder, actionMute]); } const actionMaskAsRead = (chat.unreadCount || chat.hasUnreadMark) @@ -84,18 +98,6 @@ const useChatContextActions = ({ ? { title: lang('MarkAsUnread'), icon: 'unread', handler: () => toggleChatUnread({ id: chat.id }) } : undefined; - const actionMute = isMuted - ? { - title: lang('ChatList.Unmute'), - icon: 'unmute', - handler: () => updateChatMutedState({ chatId: chat.id, isMuted: false }), - } - : { - title: lang('ChatList.Mute'), - icon: 'mute', - handler: () => updateChatMutedState({ chatId: chat.id, isMuted: true }), - }; - const actionArchive = isChatArchived(chat) ? { title: lang('Unarchive'), icon: 'unarchive', handler: () => toggleChatArchived({ id: chat.id }) } : { title: lang('Archive'), icon: 'archive', handler: () => toggleChatArchived({ id: chat.id }) }; @@ -131,7 +133,7 @@ const useChatContextActions = ({ ]) as MenuItemContextAction[]; }, [ chat, user, canChangeFolder, lang, handleChatFolderChange, isPinned, isInSearch, isMuted, - handleDelete, handleReport, folderId, isSelf, isServiceNotifications, + handleDelete, handleMute, handleReport, folderId, isSelf, isServiceNotifications, ]); };