Chat: Mute options (#3233)
This commit is contained in:
parent
f90317c650
commit
28fc59e070
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
51
src/api/gramjs/scheduleUnmute.ts
Normal file
51
src/api/gramjs/scheduleUnmute.ts
Normal 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,
|
||||
}));
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 }
|
||||
|
||||
16
src/components/left/MuteChatModal.async.tsx
Normal file
16
src/components/left/MuteChatModal.async.tsx
Normal 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);
|
||||
92
src/components/left/MuteChatModal.tsx
Normal file
92
src/components/left/MuteChatModal.tsx
Normal 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);
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user