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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ export {
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage,
deleteSavedHistory,
} from './messages'; } from './messages';
export { 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 { API_THROTTLE_RESET_UPDATES, API_UPDATE_THROTTLE } from '../../../config';
import { throttle, throttleWithTickEnd } from '../../../util/schedulers'; import { throttle, throttleWithTickEnd } from '../../../util/schedulers';
import { updateFullLocalDb } from '../localDb'; 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 initAuth } from './auth';
import { init as initBots } from './bots'; import { init as initBots } from './bots';
import { init as initCalls } from './calls'; import { init as initCalls } from './calls';

View File

@ -80,8 +80,8 @@ import {
addMessageToLocalDb, addMessageToLocalDb,
deserializeBytes, deserializeBytes,
} from '../helpers'; } from '../helpers';
import { updateChannelState } from '../updateManager'; import { processAffectedHistory, updateChannelState } from '../updates/updateManager';
import { dispatchThreadInfoUpdates } from '../updater'; import { dispatchThreadInfoUpdates } from '../updates/updater';
import { requestChatUpdate } from './chats'; import { requestChatUpdate } from './chats';
import { handleGramJsUpdate, invokeRequest, uploadFile } from './client'; import { handleGramJsUpdate, invokeRequest, uploadFile } from './client';
@ -715,10 +715,18 @@ export async function pinMessage({
} }
export async function unpinAllMessages({ chat, threadId }: { chat: ApiChat; threadId?: ThreadId }) { 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), peer: buildInputPeer(chat.id, chat.accessHash),
...(threadId && { topMsgId: Number(threadId) }), ...(threadId && { topMsgId: Number(threadId) }),
})); }));
if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await unpinAllMessages({ chat, threadId });
}
} }
export async function deleteMessages({ export async function deleteMessages({
@ -744,6 +752,8 @@ export async function deleteMessages({
return; return;
} }
processAffectedHistory(chat, result);
onUpdate({ onUpdate({
'@type': 'deleteMessages', '@type': 'deleteMessages',
ids: messageIds, ids: messageIds,
@ -784,9 +794,13 @@ export async function deleteHistory({
return; return;
} }
if ('offset' in result && result.offset) { if ('offset' in result) {
await deleteHistory({ chat, shouldDeleteForAll }); processAffectedHistory(chat, result);
return;
if (result.offset) {
await deleteHistory({ chat, shouldDeleteForAll });
return;
}
} }
onUpdate({ 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({ export async function reportMessages({
peer, messageIds, reason, description, peer, messageIds, reason, description,
}: { }: {
@ -862,10 +902,14 @@ export async function markMessageListRead({
readMaxId: fixedMaxId, readMaxId: fixedMaxId,
})); }));
} else { } else {
await invokeRequest(new GramJs.messages.ReadHistory({ const result = await invokeRequest(new GramJs.messages.ReadHistory({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
maxId: fixedMaxId, maxId: fixedMaxId,
})); }));
if (result) {
processAffectedHistory(chat, result);
}
} }
if (threadId === MAIN_THREAD_ID) { if (threadId === MAIN_THREAD_ID) {
@ -880,7 +924,7 @@ export async function markMessagesRead({
}) { }) {
const isChannel = getEntityTypeById(chat.id) === 'channel'; const isChannel = getEntityTypeById(chat.id) === 'channel';
await invokeRequest( const result = await invokeRequest(
isChannel isChannel
? new GramJs.channels.ReadMessageContents({ ? new GramJs.channels.ReadMessageContents({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, 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({ onUpdate({
...(isChannel ? { ...(isChannel ? {
'@type': 'updateChannelMessages', '@type': 'updateChannelMessages',
@ -1575,28 +1627,40 @@ export function clickSponsoredMessage({ chat, random }: { chat: ApiChat; random:
})); }));
} }
export function readAllMentions({ export async function readAllMentions({
chat, chat,
}: { }: {
chat: ApiChat; chat: ApiChat;
}) { }) {
return invokeRequest(new GramJs.messages.ReadMentions({ const result = await invokeRequest(new GramJs.messages.ReadMentions({
peer: buildInputPeer(chat.id, chat.accessHash), 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,
}: { }: {
chat: ApiChat; chat: ApiChat;
}) { }) {
return invokeRequest(new GramJs.messages.ReadReactions({ const result = await invokeRequest(new GramJs.messages.ReadReactions({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
}), { }));
shouldReturnTrue: true,
}); if (!result) return;
processAffectedHistory(chat, result);
if (result.offset) {
await readAllReactions({ chat });
}
} }
export async function fetchUnreadMentions({ 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 { Api as GramJs } from '../../../lib/gramjs';
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../lib/gramjs/network'; 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 type { Update } from './updater';
import { DEBUG } from '../../config'; import { DEBUG } from '../../../config';
import SortedQueue from '../../util/SortedQueue'; import SortedQueue from '../../../util/SortedQueue';
import { buildApiPeerId } from './apiBuilders/peers'; import { buildApiPeerId } from '../apiBuilders/peers';
import { buildInputEntity } from './gramjsBuilders'; import { buildInputEntity, buildMtpPeerId } from '../gramjsBuilders';
import { addEntitiesToLocalDb } from './helpers'; import { addEntitiesToLocalDb } from '../helpers';
import localDb from './localDb'; import localDb from '../localDb';
import { dispatchUserAndChatUpdates, sendUpdate, updater } from './updater'; import { dispatchUserAndChatUpdates, sendUpdate, updater } from './updater';
import { buildLocalUpdatePts, type UpdatePts } from './UpdatePts';
export type State = { export type State = {
seq: number; seq: number;
date: number; date: number;
@ -19,7 +22,7 @@ export type State = {
qts: number; qts: number;
}; };
type SeqUpdate = (GramJs.Updates | GramJs.UpdatesCombined) & { _isFromDifference?: true }; 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 COMMON_BOX_QUEUE_ID = '0';
const CHANNEL_DIFFERENCE_LIMIT = 1000; const CHANNEL_DIFFERENCE_LIMIT = 1000;
@ -201,10 +204,12 @@ function popPtsQueue(channelId: string) {
const pts = update.pts; const pts = update.pts;
const ptsCount = getPtsCount(update); const ptsCount = getPtsCount(update);
// Sometimes server sends updates for channels that are opened in other clients. We ignore them
if (localPts === undefined) { if (localPts === undefined) {
if (DEBUG) { if (DEBUG) {
// Uncomment to debug missing updates
// eslint-disable-next-line no-console // 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; return;
} }
@ -389,6 +394,16 @@ export function reset() {
isInited = false; 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() { async function loadRemoteState() {
const remoteState = await invoke(new GramJs.updates.GetState()); const remoteState = await invoke(new GramJs.updates.GetState());
if (!remoteState) return; 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 { import type {
ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped, ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped,
ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate, ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate,
} from '../types'; } from '../../types';
import { DEBUG, GENERAL_TOPIC_ID } from '../../config'; import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
import { compact, omit, pick } from '../../util/iteratees'; import { compact, omit, pick } from '../../../util/iteratees';
import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime'; import { getServerTimeOffset, setServerTimeOffset } from '../../../util/serverTime';
import { buildApiBotMenuButton } from './apiBuilders/bots'; import { buildApiBotMenuButton } from '../apiBuilders/bots';
import { import {
buildApiGroupCall, buildApiGroupCall,
buildApiGroupCallParticipant, buildApiGroupCallParticipant,
buildPhoneCall, buildPhoneCall,
getGroupCallId, getGroupCallId,
} from './apiBuilders/calls'; } from '../apiBuilders/calls';
import { import {
buildApiChatFolder, buildApiChatFolder,
buildApiChatFromPreview, buildApiChatFromPreview,
@ -24,15 +24,15 @@ import {
buildChatMember, buildChatMember,
buildChatMembers, buildChatMembers,
buildChatTypingStatus, buildChatTypingStatus,
} from './apiBuilders/chats'; } from '../apiBuilders/chats';
import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from './apiBuilders/common'; import { buildApiPhoto, buildApiUsernames, buildPrivacyRules } from '../apiBuilders/common';
import { omitVirtualClassFields } from './apiBuilders/helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers';
import { import {
buildApiMessageExtendedMediaPreview, buildApiMessageExtendedMediaPreview,
buildMessageMediaContent, buildMessageMediaContent,
buildPoll, buildPoll,
buildPollResults, buildPollResults,
} from './apiBuilders/messageContent'; } from '../apiBuilders/messageContent';
import { import {
buildApiMessage, buildApiMessage,
buildApiMessageFromNotification, buildApiMessageFromNotification,
@ -40,28 +40,28 @@ import {
buildApiMessageFromShortChat, buildApiMessageFromShortChat,
buildApiThreadInfoFromMessage, buildApiThreadInfoFromMessage,
buildMessageDraft, buildMessageDraft,
} from './apiBuilders/messages'; } from '../apiBuilders/messages';
import { import {
buildApiNotifyException, buildApiNotifyException,
buildApiNotifyExceptionTopic, buildApiNotifyExceptionTopic,
buildPrivacyKey, buildPrivacyKey,
} from './apiBuilders/misc'; } from '../apiBuilders/misc';
import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { import {
buildApiReaction, buildApiReaction,
buildMessageReactions, buildMessageReactions,
} from './apiBuilders/reactions'; } from '../apiBuilders/reactions';
import { buildApiStealthMode, buildApiStory } from './apiBuilders/stories'; import { buildApiStealthMode, buildApiStory } from '../apiBuilders/stories';
import { buildApiEmojiInteraction, buildStickerSet } from './apiBuilders/symbols'; import { buildApiEmojiInteraction, buildStickerSet } from '../apiBuilders/symbols';
import { import {
buildApiUser, buildApiUser,
buildApiUserStatus, buildApiUserStatus,
} from './apiBuilders/users'; } from '../apiBuilders/users';
import { import {
buildChatPhotoForLocalDb, buildChatPhotoForLocalDb,
buildMessageFromUpdate, buildMessageFromUpdate,
isMessageWithMedia, isMessageWithMedia,
} from './gramjsBuilders'; } from '../gramjsBuilders';
import { import {
addEntitiesToLocalDb, addEntitiesToLocalDb,
addMessageToLocalDb, addMessageToLocalDb,
@ -72,13 +72,15 @@ import {
resolveMessageApiChatId, resolveMessageApiChatId,
serializeBytes, serializeBytes,
swapLocalInvoiceMedia, swapLocalInvoiceMedia,
} from './helpers'; } from '../helpers';
import localDb from './localDb'; import localDb from '../localDb';
import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from './scheduleUnmute'; import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from '../scheduleUnmute';
import { LocalUpdateChannelPts, LocalUpdatePts, type UpdatePts } from './UpdatePts';
export type Update = ( export type Update = (
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
) | typeof connection.UpdateConnectionState; ) | typeof connection.UpdateConnectionState | UpdatePts;
const DELETE_MISSING_CHANNEL_MESSAGE_DELAY = 1000; const DELETE_MISSING_CHANNEL_MESSAGE_DELAY = 1000;
@ -1156,6 +1158,8 @@ export function updater(update: Update) {
chatId: buildApiPeerId(update.channelId, 'channel'), chatId: buildApiPeerId(update.channelId, 'channel'),
isEnabled: update.enabled ? true : undefined, isEnabled: update.enabled ? true : undefined,
}); });
} else if (update instanceof LocalUpdatePts || update instanceof LocalUpdateChannelPts) {
// Do nothing, handled on the manager side
} else if (DEBUG) { } else if (DEBUG) {
const params = typeof update === 'object' && 'className' in update ? update.className : update; const params = typeof update === 'object' && 'className' in update ? update.className : update;
log('UNEXPECTED UPDATE', params); log('UNEXPECTED UPDATE', params);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -464,6 +464,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break; 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': { case 'updateCommonBoxMessages': {
const { ids, messageUpdate } = update; const { ids, messageUpdate } = update;
@ -925,6 +937,29 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string, threa
return undefined; 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>( export function deleteMessages<T extends GlobalState>(
global: T, chatId: string | undefined, ids: number[], actions: RequiredGlobalActions, global: T, chatId: string | undefined, ids: number[], actions: RequiredGlobalActions,
) { ) {
@ -942,8 +977,6 @@ export function deleteMessages<T extends GlobalState>(
isDeleting: true, isDeleting: true,
}); });
global = clearMessageTranslation(global, chatId, id);
if (chat.topics?.[id]) { if (chat.topics?.[id]) {
global = deleteTopic(global, chatId, id); global = deleteTopic(global, chatId, id);
} }

View File

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

View File

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

View File

@ -45,6 +45,24 @@ const useChatContextActions = ({
const { isSelf } = user || {}; const { isSelf } = user || {};
const isServiceNotifications = user?.id === SERVICE_NOTIFICATIONS_USER_ID; 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(() => { return useMemo(() => {
if (!chat) { if (!chat) {
return undefined; return undefined;
@ -91,8 +109,15 @@ const useChatContextActions = ({
handler: togglePinned, handler: togglePinned,
}; };
const actionDelete = {
title: deleteTitle,
icon: 'delete',
destructive: true,
handler: handleDelete,
};
if (isSavedDialog) { if (isSavedDialog) {
return compact([actionOpenInNewTab, actionPin]) as MenuItemContextAction[]; return compact([actionOpenInNewTab, actionPin, actionDelete]) as MenuItemContextAction[];
} }
const actionAddToFolder = canChangeFolder ? { const actionAddToFolder = canChangeFolder ? {
@ -133,17 +158,6 @@ const useChatContextActions = ({
? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport } ? { title: lang('ReportPeer.Report'), icon: 'flag', handler: handleReport }
: undefined; : 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; const isInFolder = folderId !== undefined;
return compact([ return compact([
@ -159,7 +173,7 @@ const useChatContextActions = ({
]) as MenuItemContextAction[]; ]) as MenuItemContextAction[];
}, [ }, [
chat, user, canChangeFolder, lang, handleChatFolderChange, isPinned, isInSearch, isMuted, currentUserId, 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.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.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.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.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs;
messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;

View File

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