Saved Dialogs: Allow deletion (#4243)

This commit is contained in:
Alexander Zinchuk 2024-02-06 16:49:16 +01:00
parent 8548e96e01
commit af8aa64d8d
24 changed files with 345 additions and 109 deletions

View File

@ -158,7 +158,8 @@ export type UniversalMessage = (
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' | 'forwards' | 'silent' | 'pinned'
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' | 'forwards' | 'silent' | 'pinned' |
'savedPeerId'
)>
);
@ -197,6 +198,8 @@ export function buildApiMessageWithChatId(
const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId);
const hasComments = mtpMessage.replies?.comments;
const savedPeerId = mtpMessage.savedPeerId && getApiChatIdFromMtpPeer(mtpMessage.savedPeerId);
return omitUndefined({
id: mtpMessage.id,
chatId,
@ -233,6 +236,7 @@ export function buildApiMessageWithChatId(
isProtected,
isForwardingAllowed,
hasComments,
savedPeerId,
} satisfies ApiMessage);
}

View File

@ -72,8 +72,10 @@ import {
} from '../helpers';
import localDb from '../localDb';
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
import { applyState, processUpdate, updateChannelState } from '../updateManager';
import { dispatchThreadInfoUpdates } from '../updater';
import {
applyState, processAffectedHistory, processUpdate, updateChannelState,
} from '../updates/updateManager';
import { dispatchThreadInfoUpdates } from '../updates/updater';
import { invokeRequest, uploadFile } from './client';
type FullChatData = {
@ -1787,7 +1789,7 @@ export async function fetchTopicById({
};
}
export function deleteTopic({
export async function deleteTopic({
chat, topicId,
}: {
chat: ApiChat;
@ -1795,12 +1797,18 @@ export function deleteTopic({
}) {
const { id, accessHash } = chat;
return invokeRequest(new GramJs.channels.DeleteTopicHistory({
const result = await invokeRequest(new GramJs.channels.DeleteTopicHistory({
channel: buildInputPeer(id, accessHash),
topMsgId: topicId,
}), {
shouldReturnTrue: true,
});
}));
if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await deleteTopic({ chat, topicId });
}
}
export function togglePinnedTopic({

View File

@ -36,7 +36,7 @@ import {
reset as resetUpdatesManager,
scheduleGetChannelDifference,
updateChannelState,
} from '../updateManager';
} from '../updates/updateManager';
import {
onAuthError, onAuthReady, onCurrentUserUpdate, onRequestCode, onRequestPassword, onRequestPhoneNumber,
onRequestQrCode, onRequestRegistration, onWebAuthTokenFailed,

View File

@ -35,6 +35,7 @@ export {
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage,
deleteSavedHistory,
} from './messages';
export {

View File

@ -10,7 +10,7 @@ import type { MethodArgs, MethodResponse, Methods } from './types';
import { API_THROTTLE_RESET_UPDATES, API_UPDATE_THROTTLE } from '../../../config';
import { throttle, throttleWithTickEnd } from '../../../util/schedulers';
import { updateFullLocalDb } from '../localDb';
import { init as initUpdater } from '../updater';
import { init as initUpdater } from '../updates/updater';
import { init as initAuth } from './auth';
import { init as initBots } from './bots';
import { init as initCalls } from './calls';

View File

@ -80,8 +80,8 @@ import {
addMessageToLocalDb,
deserializeBytes,
} from '../helpers';
import { updateChannelState } from '../updateManager';
import { dispatchThreadInfoUpdates } from '../updater';
import { processAffectedHistory, updateChannelState } from '../updates/updateManager';
import { dispatchThreadInfoUpdates } from '../updates/updater';
import { requestChatUpdate } from './chats';
import { handleGramJsUpdate, invokeRequest, uploadFile } from './client';
@ -715,10 +715,18 @@ export async function pinMessage({
}
export async function unpinAllMessages({ chat, threadId }: { chat: ApiChat; threadId?: ThreadId }) {
await invokeRequest(new GramJs.messages.UnpinAllMessages({
const result = await invokeRequest(new GramJs.messages.UnpinAllMessages({
peer: buildInputPeer(chat.id, chat.accessHash),
...(threadId && { topMsgId: Number(threadId) }),
}));
if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await unpinAllMessages({ chat, threadId });
}
}
export async function deleteMessages({
@ -744,6 +752,8 @@ export async function deleteMessages({
return;
}
processAffectedHistory(chat, result);
onUpdate({
'@type': 'deleteMessages',
ids: messageIds,
@ -784,9 +794,13 @@ export async function deleteHistory({
return;
}
if ('offset' in result && result.offset) {
await deleteHistory({ chat, shouldDeleteForAll });
return;
if ('offset' in result) {
processAffectedHistory(chat, result);
if (result.offset) {
await deleteHistory({ chat, shouldDeleteForAll });
return;
}
}
onUpdate({
@ -795,6 +809,32 @@ export async function deleteHistory({
});
}
export async function deleteSavedHistory({
chat,
}: {
chat: ApiChat;
}) {
const result = await invokeRequest(new GramJs.messages.DeleteSavedHistory({
peer: buildInputPeer(chat.id, chat.accessHash),
}));
if (!result) {
return;
}
processAffectedHistory(chat, result);
if (result.offset) {
await deleteSavedHistory({ chat });
return;
}
onUpdate({
'@type': 'deleteSavedHistory',
chatId: chat.id,
});
}
export async function reportMessages({
peer, messageIds, reason, description,
}: {
@ -862,10 +902,14 @@ export async function markMessageListRead({
readMaxId: fixedMaxId,
}));
} else {
await invokeRequest(new GramJs.messages.ReadHistory({
const result = await invokeRequest(new GramJs.messages.ReadHistory({
peer: buildInputPeer(chat.id, chat.accessHash),
maxId: fixedMaxId,
}));
if (result) {
processAffectedHistory(chat, result);
}
}
if (threadId === MAIN_THREAD_ID) {
@ -880,7 +924,7 @@ export async function markMessagesRead({
}) {
const isChannel = getEntityTypeById(chat.id) === 'channel';
await invokeRequest(
const result = await invokeRequest(
isChannel
? new GramJs.channels.ReadMessageContents({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
@ -891,6 +935,14 @@ export async function markMessagesRead({
}),
);
if (!result) {
return;
}
if (result !== true) {
processAffectedHistory(chat, result);
}
onUpdate({
...(isChannel ? {
'@type': 'updateChannelMessages',
@ -1575,28 +1627,40 @@ export function clickSponsoredMessage({ chat, random }: { chat: ApiChat; random:
}));
}
export function readAllMentions({
export async function readAllMentions({
chat,
}: {
chat: ApiChat;
}) {
return invokeRequest(new GramJs.messages.ReadMentions({
const result = await invokeRequest(new GramJs.messages.ReadMentions({
peer: buildInputPeer(chat.id, chat.accessHash),
}), {
shouldReturnTrue: true,
});
}));
if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await readAllMentions({ chat });
}
}
export function readAllReactions({
export async function readAllReactions({
chat,
}: {
chat: ApiChat;
}) {
return invokeRequest(new GramJs.messages.ReadReactions({
const result = await invokeRequest(new GramJs.messages.ReadReactions({
peer: buildInputPeer(chat.id, chat.accessHash),
}), {
shouldReturnTrue: true,
});
}));
if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await readAllReactions({ chat });
}
}
export async function fetchUnreadMentions({

View File

@ -0,0 +1,16 @@
/* eslint-disable max-classes-per-file */
import type { BigInteger } from 'big-integer';
export class LocalUpdatePts {
constructor(public pts: number, public ptsCount: number) {}
}
export class LocalUpdateChannelPts {
constructor(public channelId: BigInteger, public pts: number, public ptsCount: number) {}
}
export type UpdatePts = LocalUpdatePts | LocalUpdateChannelPts;
export function buildLocalUpdatePts(pts: number, ptsCount: number, channelId?: BigInteger) {
return channelId ? new LocalUpdateChannelPts(channelId, pts, ptsCount) : new LocalUpdatePts(pts, ptsCount);
}

View File

@ -1,17 +1,20 @@
import { Api as GramJs } from '../../lib/gramjs';
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../lib/gramjs/network';
import { Api as GramJs } from '../../../lib/gramjs';
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gramjs/network';
import type { invokeRequest } from './methods/client';
import type { ApiChat } from '../../types';
import type { invokeRequest } from '../methods/client';
import type { Update } from './updater';
import { DEBUG } from '../../config';
import SortedQueue from '../../util/SortedQueue';
import { buildApiPeerId } from './apiBuilders/peers';
import { buildInputEntity } from './gramjsBuilders';
import { addEntitiesToLocalDb } from './helpers';
import localDb from './localDb';
import { DEBUG } from '../../../config';
import SortedQueue from '../../../util/SortedQueue';
import { buildApiPeerId } from '../apiBuilders/peers';
import { buildInputEntity, buildMtpPeerId } from '../gramjsBuilders';
import { addEntitiesToLocalDb } from '../helpers';
import localDb from '../localDb';
import { dispatchUserAndChatUpdates, sendUpdate, updater } from './updater';
import { buildLocalUpdatePts, type UpdatePts } from './UpdatePts';
export type State = {
seq: number;
date: number;
@ -19,7 +22,7 @@ export type State = {
qts: number;
};
type SeqUpdate = (GramJs.Updates | GramJs.UpdatesCombined) & { _isFromDifference?: true };
type PtsUpdate = GramJs.TypeUpdate & { pts: number } & { _isFromDifference?: true };
type PtsUpdate = ((GramJs.TypeUpdate & { pts: number }) | UpdatePts) & { _isFromDifference?: true };
const COMMON_BOX_QUEUE_ID = '0';
const CHANNEL_DIFFERENCE_LIMIT = 1000;
@ -201,10 +204,12 @@ function popPtsQueue(channelId: string) {
const pts = update.pts;
const ptsCount = getPtsCount(update);
// Sometimes server sends updates for channels that are opened in other clients. We ignore them
if (localPts === undefined) {
if (DEBUG) {
// Uncomment to debug missing updates
// eslint-disable-next-line no-console
console.error('[UpdateManager] Got pts update without local state', channelId);
// console.error('[UpdateManager] Got pts update without local state', channelId);
}
return;
}
@ -389,6 +394,16 @@ export function reset() {
isInited = false;
}
export function processAffectedHistory(
chat: ApiChat, affected: GramJs.messages.AffectedMessages | GramJs.messages.AffectedHistory,
) {
const isChannel = chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup';
const channeId = isChannel ? buildMtpPeerId(chat.id, 'channel') : undefined;
const update = buildLocalUpdatePts(affected.pts, affected.ptsCount, channeId);
processUpdate(update);
}
async function loadRemoteState() {
const remoteState = await invoke(new GramJs.updates.GetState());
if (!remoteState) return;

View File

@ -1,21 +1,21 @@
import { Api as GramJs, connection } from '../../lib/gramjs';
import { Api as GramJs, connection } from '../../../lib/gramjs';
import type { GroupCallConnectionData } from '../../lib/secret-sauce';
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
import type {
ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped,
ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate,
} from '../types';
} from '../../types';
import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
import { compact, omit, pick } from '../../util/iteratees';
import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime';
import { buildApiBotMenuButton } from './apiBuilders/bots';
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
import { compact, omit, pick } from '../../../util/iteratees';
import { getServerTimeOffset, setServerTimeOffset } from '../../../util/serverTime';
import { buildApiBotMenuButton } from '../apiBuilders/bots';
import {
buildApiGroupCall,
buildApiGroupCallParticipant,
buildPhoneCall,
getGroupCallId,
} from './apiBuilders/calls';
} from '../apiBuilders/calls';
import {
buildApiChatFolder,
buildApiChatFromPreview,
@ -24,15 +24,15 @@ import {
buildChatMember,
buildChatMembers,
buildChatTypingStatus,
} from './apiBuilders/chats';
import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from './apiBuilders/common';
import { omitVirtualClassFields } from './apiBuilders/helpers';
} from '../apiBuilders/chats';
import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from '../apiBuilders/common';
import { omitVirtualClassFields } from '../apiBuilders/helpers';
import {
buildApiMessageExtendedMediaPreview,
buildMessageMediaContent,
buildPoll,
buildPollResults,
} from './apiBuilders/messageContent';
} from '../apiBuilders/messageContent';
import {
buildApiMessage,
buildApiMessageFromNotification,
@ -40,28 +40,28 @@ import {
buildApiMessageFromShortChat,
buildApiThreadInfoFromMessage,
buildMessageDraft,
} from './apiBuilders/messages';
} from '../apiBuilders/messages';
import {
buildApiNotifyException,
buildApiNotifyExceptionTopic,
buildPrivacyKey,
} from './apiBuilders/misc';
import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
} from '../apiBuilders/misc';
import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import {
buildApiReaction,
buildMessageReactions,
} from './apiBuilders/reactions';
import { buildApiStealthMode, buildApiStory } from './apiBuilders/stories';
import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols';
} from '../apiBuilders/reactions';
import { buildApiStealthMode, buildApiStory } from '../apiBuilders/stories';
import { buildApiEmojiInteraction, buildStickerSet } from '../apiBuilders/symbols';
import {
buildApiUser,
buildApiUserStatus,
} from './apiBuilders/users';
} from '../apiBuilders/users';
import {
buildChatPhotoForLocalDb,
buildMessageFromUpdate,
isMessageWithMedia,
} from './gramjsBuilders';
} from '../gramjsBuilders';
import {
addEntitiesToLocalDb,
addMessageToLocalDb,
@ -72,13 +72,15 @@ import {
resolveMessageApiChatId,
serializeBytes,
swapLocalInvoiceMedia,
} from './helpers';
import localDb from './localDb';
import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from './scheduleUnmute';
} from '../helpers';
import localDb from '../localDb';
import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from '../scheduleUnmute';
import { LocalUpdateChannelPts, LocalUpdatePts, type UpdatePts } from './UpdatePts';
export type Update = (
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
) | typeof connection.UpdateConnectionState;
) | typeof connection.UpdateConnectionState | UpdatePts;
const DELETE_MISSING_CHANNEL_MESSAGE_DELAY = 1000;
@ -1156,6 +1158,8 @@ export function updater(update: Update) {
chatId: buildApiPeerId(update.channelId, 'channel'),
isEnabled: update.enabled ? true : undefined,
});
} else if (update instanceof LocalUpdatePts || update instanceof LocalUpdateChannelPts) {
// Do nothing, handled on the manager side
} else if (DEBUG) {
const params = typeof update === 'object' && 'className' in update ? update.className : update;
log('UNEXPECTED UPDATE', params);

View File

@ -536,6 +536,7 @@ export interface ApiMessage {
};
reactions?: ApiReactions;
hasComments?: boolean;
savedPeerId?: string;
}
export interface ApiReactions {

View File

@ -315,6 +315,11 @@ export type ApiUpdateDeleteHistory = {
chatId: string;
};
export type ApiUpdateDeleteSavedHistory = {
'@type': 'deleteSavedHistory';
chatId: string;
};
export type ApiUpdateDeleteProfilePhotos = {
'@type': 'deleteProfilePhotos';
ids: string[];
@ -730,7 +735,8 @@ export type ApiUpdate = (
ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction |
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
ApiUpdateDeleteSavedHistory
);
export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -28,6 +28,7 @@ import './DeleteChatModal.scss';
export type OwnProps = {
isOpen: boolean;
chat: ApiChat;
isSavedDialog?: boolean;
onClose: () => void;
onCloseAnimationEnd?: () => void;
};
@ -47,6 +48,7 @@ type StateProps = {
const DeleteChatModal: FC<OwnProps & StateProps> = ({
isOpen,
chat,
isSavedDialog,
isChannel,
isPrivateChat,
isChatWithSelf,
@ -62,6 +64,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
const {
leaveChannel,
deleteHistory,
deleteSavedHistory,
deleteChannel,
deleteChatUser,
blockUser,
@ -74,7 +77,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
onClose();
}, [deleteHistory, chat.id, onClose]);
}, [chat.id, onClose]);
const handleDeleteAndStop = useCallback(() => {
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
@ -84,7 +87,9 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
}, [chat.id, onClose]);
const handleDeleteChat = useCallback(() => {
if (isPrivateChat) {
if (isSavedDialog) {
deleteSavedHistory({ chatId: chat.id });
} else if (isPrivateChat) {
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
} else if (isBasicGroup) {
deleteChatUser({ chatId: chat.id, userId: currentUserId! });
@ -103,11 +108,8 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
currentUserId,
chat.isCreator,
chat.id,
isSavedDialog,
onClose,
deleteHistory,
deleteChatUser,
leaveChannel,
deleteChannel,
]);
const handleLeaveChat = useCallback(() => {
@ -133,6 +135,10 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
}
function renderTitle() {
if (isSavedDialog) {
return isChatWithSelf ? 'ClearHistoryMyNotesTitle' : 'ClearHistoryTitleSingle2';
}
if (isChannel && !chat.isCreator) {
return 'LeaveChannel';
}
@ -149,6 +155,16 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
}
function renderContent() {
if (isSavedDialog) {
return (
<p>
{renderText(
isChatWithSelf ? lang('ClearHistoryMyNotesMessage') : lang('ClearHistoryMessageSingle', chatTitle),
['simple_markdown', 'emoji'],
)}
</p>
);
}
if (isChannel && chat.isCreator) {
return (
<p>
@ -165,6 +181,10 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
}
function renderActionText() {
if (isSavedDialog) {
return 'Delete';
}
if (isChannel && !chat.isCreator) {
return 'LeaveChannel';
}
@ -189,7 +209,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
>
{renderContent()}
<div className="dialog-buttons-column">
{isBot && (
{isBot && !isSavedDialog && (
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteAndStop}>
{lang('DeleteAndStop')}
</Button>
@ -199,7 +219,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
{contactName ? renderText(lang('ChatList.DeleteForEveryone', contactName)) : lang('DeleteForAll')}
</Button>
)}
{!isPrivateChat && chat.isCreator && (
{!isPrivateChat && chat.isCreator && !isSavedDialog && (
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChat}>
{lang('DeleteForAll')}
</Button>
@ -208,7 +228,7 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
color="danger"
className="confirm-dialog-button"
isText
onClick={isPrivateChat ? handleDeleteChat : handleLeaveChat}
onClick={(isPrivateChat || isSavedDialog) ? handleDeleteChat : handleLeaveChat}
>
{lang(renderActionText())}
</Button>
@ -219,12 +239,12 @@ const DeleteChatModal: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(global, { chat }): StateProps => {
(global, { chat, isSavedDialog }): StateProps => {
const isPrivateChat = isUserId(chat.id);
const isChatWithSelf = selectIsChatWithSelf(global, chat.id);
const user = isPrivateChat && selectUser(global, getPrivateChatUserId(chat)!);
const isBot = user && isUserBot(user) && !chat.isSupport;
const canDeleteForAll = (isPrivateChat && !isChatWithSelf && !isBot);
const canDeleteForAll = (isPrivateChat && !isChatWithSelf && !isBot && !isSavedDialog);
const contactName = isPrivateChat
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
: undefined;

View File

@ -354,6 +354,7 @@ const Chat: FC<OwnProps & StateProps> = ({
onClose={closeDeleteModal}
onCloseAnimationEnd={unmarkRenderDeleteModal}
chat={chat}
isSavedDialog={isSavedDialog}
/>
)}
{shouldRenderMuteModal && (

View File

@ -12,6 +12,7 @@ import { ManagementScreens } from '../../types';
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
import {
getHasAdminRight,
getIsSavedDialog,
isAnonymousForwardsChat,
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers';
@ -478,6 +479,8 @@ export default memo(withGlobal<OwnProps>(
const isDiscussionThread = messageListType === 'thread' && threadId !== MAIN_THREAD_ID;
const isRightColumnShown = selectIsRightColumnShown(global, isMobile);
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const isUserBlocked = isPrivate ? selectIsUserBlocked(global, chatId) : false;
const canRestartBot = Boolean(bot && isUserBlocked);
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
@ -489,7 +492,7 @@ export default memo(withGlobal<OwnProps>(
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot && !chat.isSupport
&& !isAnonymousForwardsChat(chat.id);
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
const canLeave = isMainThread && !canSubscribe;
const canLeave = isSavedDialog || (isMainThread && !canSubscribe);
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && chat.isCallActive;
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && !chat.isCallActive
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));

View File

@ -15,6 +15,7 @@ import {
getCanDeleteChat,
getCanManageTopic,
getHasAdminRight,
getIsSavedDialog,
isChatChannel,
isChatGroup,
isUserId,
@ -119,6 +120,7 @@ type StateProps = {
isBlocked?: boolean;
isBot?: boolean;
isChatWithSelf?: boolean;
savedDialog?: ApiChat;
};
const CLOSE_MENU_ANIMATION_DURATION = 200;
@ -163,6 +165,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
isBlocked,
isBot,
isChatWithSelf,
savedDialog,
onJoinRequestsClick,
onSubscribeChannel,
onSearchClick,
@ -403,6 +406,24 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
});
}, [botCommands, closeMenu, lang, sendBotCommand]);
const deleteTitle = useMemo(() => {
if (!chat) return undefined;
if (isPrivate || savedDialog) {
return lang('DeleteChatUser');
}
if (canDeleteChat) {
return lang('GroupInfo.DeleteAndExit');
}
if (isChannel) {
return lang('LeaveChannel');
}
return lang('Group.LeaveGroup');
}, [canDeleteChat, chat, isChannel, isPrivate, savedDialog, lang]);
return (
<Portal>
<div className="HeaderMenuContainer">
@ -627,9 +648,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
icon="delete"
onClick={handleDelete}
>
{lang(isPrivate
? 'DeleteChatUser'
: (canDeleteChat ? 'GroupInfo.DeleteAndExit' : (isChannel ? 'LeaveChannel' : 'Group.LeaveGroup')))}
{deleteTitle}
</MenuItem>
</>
)}
@ -638,7 +657,8 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
<DeleteChatModal
isOpen={isDeleteModalOpen}
onClose={closeDeleteModal}
chat={chat}
chat={savedDialog || chat}
isSavedDialog={Boolean(savedDialog)}
/>
)}
{canMute && shouldRenderMuteModal && chat?.id && (
@ -694,6 +714,9 @@ export default memo(withGlobal<OwnProps>(
// Context menu item should only be displayed if user hid translation panel
const canTranslate = selectCanTranslateChat(global, chatId) && fullInfo?.isTranslationDisabled;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const savedDialog = isSavedDialog ? selectChat(global, String(threadId)) : undefined;
return {
chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
@ -717,6 +740,7 @@ export default memo(withGlobal<OwnProps>(
isBlocked: userFullInfo?.isBlocked,
isBot: Boolean(chatBot),
isChatWithSelf,
savedDialog,
};
},
)(HeaderMenuContainer));

View File

@ -583,7 +583,6 @@ addActionHandler('loadTopChats', (): ActionReturnType => {
runThrottledForLoadTopChats(() => {
loadChats('active');
loadChats('archived');
loadChats('saved');
});
});
@ -2244,9 +2243,7 @@ addActionHandler('deleteTopic', async (global, actions, payload): Promise<void>
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('deleteTopic', { chat, topicId });
if (!result) return;
await callApi('deleteTopic', { chat, topicId });
global = getGlobal();
global = deleteTopic(global, chatId, topicId);

View File

@ -710,6 +710,22 @@ addActionHandler('deleteHistory', async (global, actions, payload): Promise<void
});
});
addActionHandler('deleteSavedHistory', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload!;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
await callApi('deleteSavedHistory', { chat });
global = getGlobal();
const activeChat = selectCurrentMessageList(global, tabId);
if (activeChat && activeChat.threadId === chatId) {
actions.openChat({ id: undefined, tabId });
}
});
addActionHandler('reportMessages', async (global, actions, payload): Promise<void> => {
const {
messageIds, reason, description, tabId = getCurrentTabId(),

View File

@ -464,6 +464,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break;
}
case 'deleteSavedHistory': {
const { chatId } = update;
const currentUserId = global.currentUserId!;
global = removeChatFromChatLists(global, chatId, 'saved');
setGlobal(global);
global = getGlobal();
deleteThread(global, currentUserId, chatId, actions);
break;
}
case 'updateCommonBoxMessages': {
const { ids, messageUpdate } = update;
@ -925,6 +937,29 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string, threa
return undefined;
}
export function deleteThread<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
actions: RequiredGlobalActions,
) {
const byId = selectChatMessages(global, chatId);
if (!byId) {
return;
}
const messageIds = Object.values(byId).filter((message) => {
const messageThreadId = selectThreadIdFromMessage(global, message);
return messageThreadId === threadId;
}).map((message) => message.id);
if (!messageIds.length) {
return;
}
deleteMessages(global, chatId, messageIds, actions);
}
export function deleteMessages<T extends GlobalState>(
global: T, chatId: string | undefined, ids: number[], actions: RequiredGlobalActions,
) {
@ -942,8 +977,6 @@ export function deleteMessages<T extends GlobalState>(
isDeleting: true,
});
global = clearMessageTranslation(global, chatId, id);
if (chat.topics?.[id]) {
global = deleteTopic(global, chatId, id);
}

View File

@ -36,6 +36,7 @@ import {
selectViewportIds,
} from '../selectors';
import { updateTabState } from './tabs';
import { clearMessageTranslation } from './translations';
type MessageStoreSections = {
byId: Record<number, ApiMessage>;
@ -261,10 +262,12 @@ export function deleteChatMessages<T extends GlobalState>(
const message = byId[messageId];
if (!message) return;
const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) return;
if (!threadId) return;
const threadMessages = updatedThreads.get(threadId) || [];
threadMessages.push(messageId);
updatedThreads.set(threadId, threadMessages);
global = clearMessageTranslation(global, chatId, messageId);
});
const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter(
@ -297,15 +300,11 @@ export function deleteChatMessages<T extends GlobalState>(
}
Object.values(global.byTabId).forEach(({ id: tabId }) => {
let viewportIds = selectViewportIds(global, chatId, threadId, tabId);
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
if (!viewportIds) return;
messageIds.forEach((messageId) => {
if (viewportIds?.includes(messageId)) {
viewportIds = viewportIds.filter((id) => id !== messageId);
}
});
global = replaceTabThreadParam(global, chatId, threadId, 'viewportIds', viewportIds, tabId);
const newViewportIds = excludeSortedArray(viewportIds, messageIds);
global = replaceTabThreadParam(global, chatId, threadId, 'viewportIds', newViewportIds, tabId);
});
global = replaceThreadParam(global, chatId, threadId, 'listedIds', listedIds);

View File

@ -508,7 +508,12 @@ export function selectCanDeleteTopic<T extends GlobalState>(global: T, chatId: s
export function selectSavedDialogIdFromMessage<T extends GlobalState>(
global: T, message: ApiMessage,
): string | undefined {
const { chatId, senderId, forwardInfo } = message;
const {
chatId, senderId, forwardInfo, savedPeerId,
} = message;
if (savedPeerId) return savedPeerId;
if (chatId !== global.currentUserId) {
return undefined;
}

View File

@ -1385,6 +1385,9 @@ export interface ActionPayloads {
chatId: string;
shouldDeleteForAll?: boolean;
} & WithTabId;
deleteSavedHistory: {
chatId: string;
} & WithTabId;
loadSponsoredMessages: {
chatId: string;
};

View File

@ -45,6 +45,24 @@ const useChatContextActions = ({
const { isSelf } = user || {};
const isServiceNotifications = user?.id === SERVICE_NOTIFICATIONS_USER_ID;
const deleteTitle = useMemo(() => {
if (!chat) return undefined;
if (isUserId(chat.id) || isSavedDialog) {
return lang('DeleteChatUser');
}
if (getCanDeleteChat(chat)) {
return lang('DeleteChat');
}
if (isChatChannel(chat)) {
return lang('LeaveChannel');
}
return lang('Group.LeaveGroup');
}, [chat, isSavedDialog, lang]);
return useMemo(() => {
if (!chat) {
return undefined;
@ -91,8 +109,15 @@ const useChatContextActions = ({
handler: togglePinned,
};
const actionDelete = {
title: deleteTitle,
icon: 'delete',
destructive: true,
handler: handleDelete,
};
if (isSavedDialog) {
return compact([actionOpenInNewTab, actionPin]) as MenuItemContextAction[];
return compact([actionOpenInNewTab, actionPin, actionDelete]) as MenuItemContextAction[];
}
const actionAddToFolder = canChangeFolder ? {
@ -133,17 +158,6 @@ const useChatContextActions = ({
? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport }
: undefined;
const actionDelete = {
title: isUserId(chat.id)
? lang('Delete')
: lang(getCanDeleteChat(chat)
? 'DeleteChat'
: (isChatChannel(chat) ? 'LeaveChannel' : 'Group.LeaveGroup')),
icon: 'delete',
destructive: true,
handler: handleDelete,
};
const isInFolder = folderId !== undefined;
return compact([
@ -159,7 +173,7 @@ const useChatContextActions = ({
]) as MenuItemContextAction[];
}, [
chat, user, canChangeFolder, lang, handleChatFolderChange, isPinned, isInSearch, isMuted, currentUserId,
handleDelete, handleMute, handleReport, folderId, isSelf, isServiceNotifications, isSavedDialog,
handleDelete, handleMute, handleReport, folderId, isSelf, isServiceNotifications, isSavedDialog, deleteTitle,
]);
};

View File

@ -1404,6 +1404,7 @@ messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp;
messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult;
messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs;
messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;
messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory;
messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs;
messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
updates.getState#edd4882a = updates.State;

View File

@ -284,6 +284,7 @@
"messages.getUnreadMentions",
"messages.getSavedDialogs",
"messages.getSavedHistory",
"messages.deleteSavedHistory",
"messages.getPinnedSavedDialogs",
"messages.toggleSavedDialogPin",
"help.getPremiumPromo",