Chat: Mute options (#3233)

This commit is contained in:
Alexander Zinchuk 2023-06-02 15:06:23 +02:00
parent f90317c650
commit 28fc59e070
17 changed files with 368 additions and 75 deletions

View File

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

View File

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

View File

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

View File

@ -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<string, any>();
const unmuteQueue: Array<UnmuteQueueItem> = [];
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,
}));
}

View File

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

View File

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

View File

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

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const MuteChatModal = useModuleLoader(Bundles.Extra, 'MuteChatModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return MuteChatModal ? <MuteChatModal {...props} /> : undefined;
};
export default memo(MuteChatModalAsync);

View File

@ -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<OwnProps> = ({
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 (
<Modal
isOpen={isOpen}
onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd}
onEnter={handleSubmit}
className="delete"
title={lang('Notifications')}
>
<RadioGroup
name="muteFor"
options={muteForOptions}
selected={muteUntilOption}
onChange={setMuteUntilOption as any}
/>
<div className="dialog-buttons">
<Button color="primary" className="confirm-dialog-button" isText onClick={handleSubmit}>
{lang('Common.Done')}
</Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
</div>
</Modal>
);
};
export default memo(MuteChatModal);

View File

@ -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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
openDeleteModal();
}, [markRenderDeleteModal, openDeleteModal]);
const handleMute = useCallback(() => {
markRenderMuteModal();
openMuteModal();
}, [markRenderMuteModal, openMuteModal]);
const handleChatFolderChange = useCallback(() => {
markRenderChatFolderModal();
openChatFolderModal();
@ -197,6 +205,7 @@ const Chat: FC<OwnProps & StateProps> = ({
chat,
user,
handleDelete,
handleMute,
handleChatFolderChange,
handleReport,
folderId,
@ -281,6 +290,14 @@ const Chat: FC<OwnProps & StateProps> = ({
chat={chat}
/>
)}
{shouldRenderMuteModal && (
<MuteChatModal
isOpen={isMuteModalOpen}
onClose={closeMuteModal}
onCloseAnimationEnd={unmarkRenderMuteModal}
chatId={chatId}
/>
)}
{shouldRenderChatFolderModal && (
<ChatFolderModal
isOpen={isChatFolderModalOpen}

View File

@ -36,6 +36,7 @@ import useLang from '../../../hooks/useLang';
import ListItem from '../../ui/ListItem';
import LastMessageMeta from '../../common/LastMessageMeta';
import ChatBadge from './ChatBadge';
import MuteChatModal from '../MuteChatModal.async';
import ConfirmDialog from '../../ui/ConfirmDialog';
import TopicIcon from '../../common/TopicIcon';
@ -95,7 +96,9 @@ const Topic: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}
}, [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 (
<ListItem
@ -188,7 +203,6 @@ const Topic: FC<OwnProps & StateProps> = ({
/>
</div>
</div>
{shouldRenderDeleteModal && (
<ConfirmDialog
isOpen={isDeleteModalOpen}
@ -200,6 +214,15 @@ const Topic: FC<OwnProps & StateProps> = ({
confirmLabel={lang('Delete')}
/>
)}
{shouldRenderMuteModal && (
<MuteChatModal
isOpen={isMuteModalOpen}
onClose={closeMuteModal}
onCloseAnimationEnd={unmarkRenderMuteModal}
chatId={chatId}
topicId={topic.id}
/>
)}
</ListItem>
);
};

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
isPinned,
isMuted,
canChangeFolder,
handleDelete: openDeleteModal,
handleChatFolderChange: openChatFolderModal,
handleMute,
handleChatFolderChange,
}, true);
const handleClick = useCallback(() => {
@ -77,16 +89,22 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
) : (
<GroupChatInfo chatId={chatId} withUsername={withUsername} avatarSize="large" />
)}
<DeleteChatModal
isOpen={isDeleteModalOpen}
onClose={closeDeleteModal}
chat={chat}
/>
<ChatFolderModal
isOpen={isChatFolderModalOpen}
onClose={closeChatFolderModal}
chatId={chatId}
/>
{shouldRenderMuteModal && (
<MuteChatModal
isOpen={isMuteModalOpen}
onClose={closeMuteModal}
onCloseAnimationEnd={unmarkRenderMuteModal}
chatId={chatId}
/>
)}
{shouldRenderChatFolderModal && (
<ChatFolderModal
isOpen={isChatFolderModalOpen}
onClose={closeChatFolderModal}
onCloseAnimationEnd={unmarkRenderChatFolderModal}
chatId={chatId}
/>
)}
</ListItem>
);
};

View File

@ -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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onClose();
}, [onClose]);
const closeMuteModal = useCallback(() => {
setIsMuteModalOpen(false);
onClose();
}, [onClose]);
const handleDelete = useCallback(() => {
setIsMenuOpen(false);
setIsDeleteModalOpen(true);
@ -215,10 +224,16 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
{lang('VideoCall')}
</MenuItem>
)}
{canMute && (
{canMute && (isMuted ? (
<MenuItem
icon={isMuted ? 'unmute' : 'mute'}
onClick={handleToggleMuteClick}
icon="unmute"
onClick={handleUnmuteClick}
>
{lang(isMuted ? 'ChatsUnmute' : 'ChatsMute')}
{lang('ChatsUnmute')}
</MenuItem>
)
: (
<MenuItem
icon="mute"
onClick={handleMuteClick}
>
{lang('ChatsMute')}...
</MenuItem>
)
)}
{(canEnterVoiceChat || canCreateVoiceChat) && (
<MenuItem
@ -525,6 +549,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
chat={chat}
/>
)}
{canMute && shouldRenderMuteModal && chat?.id && (
<MuteChatModal
isOpen={isMuteModalOpen}
onClose={closeMuteModal}
onCloseAnimationEnd={unmarkRenderMuteModal}
chatId={chat.id}
/>
)}
{canReportChat && chat?.id && (
<ReportModal
isOpen={isReportModalOpen}

View File

@ -349,28 +349,32 @@ addActionHandler('requestChatUpdate', (global, actions, payload): ActionReturnTy
});
addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => {
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,
});
});

View File

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

View File

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