/*
 * https://github.com/morethanwords/tweb
 * Copyright (C) 2019-2021 Eduard Kuzmenko
 * https://github.com/morethanwords/tweb/blob/master/LICENSE
 * 
 * Originally from:
 * https://github.com/zhukov/webogram
 * Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
 * https://github.com/zhukov/webogram/blob/master/LICENSE
 */

import { LazyLoadQueueBase } from "../../components/lazyLoadQueue";
import ProgressivePreloader from "../../components/preloader";
import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise";
import { tsNow } from "../../helpers/date";
import { createPosterForVideo } from "../../helpers/files";
import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string";
import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update } from "../../layer";
import { InvokeApiOptions } from "../../types";
import I18n, { i18n, join, langPack, LangPackKey, _i18n } from "../langPack";
import { logger, LogLevels } from "../logger";
import type { ApiFileManager } from '../mtproto/apiFileManager';
//import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
import referenceDatabase, { ReferenceContext } from "../mtproto/referenceDatabase";
import serverTimeManager from "../mtproto/serverTimeManager";
import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
import searchIndexManager from '../searchIndexManager';
import DialogsStorage from "../storages/dialogs";
import FiltersStorage from "../storages/filters";
//import { telegramMeWebService } from "../mtproto/mtproto";
import apiUpdatesManager from "./apiUpdatesManager";
import appChatsManager from "./appChatsManager";
import appDocsManager, { MyDocument } from "./appDocsManager";
import appDownloadManager from "./appDownloadManager";
import appPeersManager from "./appPeersManager";
import appPhotosManager, { MyPhoto } from "./appPhotosManager";
import appPollsManager from "./appPollsManager";
import appStateManager from "./appStateManager";
import appUsersManager from "./appUsersManager";
import appWebPagesManager from "./appWebPagesManager";
import appDraftsManager from "./appDraftsManager";
import pushHeavyTask from "../../helpers/heavyQueue";
import { getFileNameByLocation } from "../../helpers/fileName";
import appProfileManager from "./appProfileManager";
import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug";
import SlicedArray, { Slice, SliceEnd } from "../../helpers/slicedArray";
import appNotificationsManager, { NotifyOptions } from "./appNotificationsManager";
import PeerTitle from "../../components/peerTitle";
import { forEachReverse } from "../../helpers/array";
import { htmlToDocumentFragment, htmlToSpan } from "../../helpers/dom";

//console.trace('include');
// TODO: если удалить сообщение в непрогруженном диалоге, то при обновлении, из-за стейта, последнего сообщения в чатлисте не будет
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках

const APITIMEOUT = 0;

export type HistoryStorage = {
  count: number | null,
  history: SlicedArray,

  maxId?: number,
  readPromise?: Promise<void>,
  readMaxId?: number,
  readOutboxMaxId?: number,

  maxOutId?: number,
  reply_markup?: any
};

export type HistoryResult = {
  count: number,
  history: Slice,
  offsetIdOffset?: number,
};

export type Dialog = MTDialog.dialog;

export type MyMessage = Message.message | Message.messageService;
export type MyInputMessagesFilter = 'inputMessagesFilterEmpty' 
  | 'inputMessagesFilterPhotos' 
  | 'inputMessagesFilterPhotoVideo' 
  | 'inputMessagesFilterVideo' 
  | 'inputMessagesFilterDocument' 
  | 'inputMessagesFilterVoice' 
  | 'inputMessagesFilterRoundVoice' 
  | 'inputMessagesFilterRoundVideo' 
  | 'inputMessagesFilterMusic' 
  | 'inputMessagesFilterUrl' 
  | 'inputMessagesFilterMyMentions'
  | 'inputMessagesFilterChatPhotos'
  | 'inputMessagesFilterPinned';

export type PinnedStorage = Partial<{
  promise: Promise<PinnedStorage>,
  count: number,
  maxId: number
}>;
export type MessagesStorage = {
  //generateIndex: (message: any) => void
  [mid: string]: any
};

export type MyMessageActionType = Message.messageService['action']['_'];

export class AppMessagesManager {
  public static MESSAGE_ID_INCREMENT = 0x10000;
  public static MESSAGE_ID_OFFSET = 0xFFFFFFFF;

  public messagesStorageByPeerId: {[peerId: string]: MessagesStorage} = {};
  public groupedMessagesStorage: {[groupId: string]: MessagesStorage} = {}; // will be used for albums
  public scheduledMessagesStorage: {[peerId: string]: MessagesStorage} = {};
  public historiesStorage: {
    [peerId: string]: HistoryStorage
  } = {};
  public threadsStorage: {
    [peerId: string]: {
      [threadId: string]: HistoryStorage
    }
  } = {};
  public searchesStorage: {
    [peerId: string]: Partial<{
      [inputFilter in MyInputMessagesFilter]: {
        count?: number,
        history: number[]
      }
    }>
  } = {};
  public pinnedMessages: {[peerId: string]: PinnedStorage} = {};

  public threadsServiceMessagesIdsStorage: {[peerId_threadId: string]: number} = {};
  public threadsToReplies: {
    [peerId_threadId: string]: string;
  } = {};

  public pendingByRandomId: {
    [randomId: string]: {
      peerId: number,
      tempId: number,
      storage: MessagesStorage
    }
  } = {};
  public pendingByMessageId: {[mid: string]: string} = {};
  public pendingAfterMsgs: any = {};
  public pendingTopMsgs: {[peerId: string]: number} = {};
  public sendFilePromise: CancellablePromise<void> = Promise.resolve();
  public tempNum = 0;
  public tempFinalizeCallbacks: {
    [tempId: string]: {
      [callbackName: string]: Partial<{
        deferred: CancellablePromise<void>, 
        callback: (message: any) => Promise<any>
      }>
    }
  } = {};
  
  public sendSmthLazyLoadQueue = new LazyLoadQueueBase(1);

  public needSingleMessages: {[peerId: string]: number[]} = {};
  private fetchSingleMessagesPromise: Promise<void> = null;

  public maxSeenId = 0;

  public migratedFromTo: {[peerId: number]: number} = {};
  public migratedToFrom: {[peerId: number]: number} = {};

  public newMessagesHandlePromise = 0;
  public newMessagesToHandle: {[peerId: string]: number[]} = {};
  public newDialogsHandlePromise = 0;
  public newDialogsToHandle: {[peerId: string]: {reload: true} | Dialog} = {};
  public newUpdatesAfterReloadToHandle: {[peerId: string]: Set<any>} = {};

  private notificationsHandlePromise = 0;
  private notificationsToHandle: {[peerId: string]: {
    fwdCount: number,
    fromId: number,
    topMessage?: MyMessage
  }} = {};

  private reloadConversationsPromise: Promise<void>;
  private reloadConversationsPeers: number[] = [];

  private dialogsIndex = searchIndexManager.createIndex();
  private cachedResults: {
    query: string,
    count: number,
    dialogs: Dialog[],
    folderId: number
  } = {
    query: '',
    count: 0,
    dialogs: [],
    folderId: 0
  };

  private log = logger('MESSAGES', LogLevels.error | LogLevels.debug | LogLevels.log | LogLevels.warn);

  public dialogsStorage: DialogsStorage;
  public filtersStorage: FiltersStorage;

  private groupedTempId = 0;

  constructor() {
    this.dialogsStorage = new DialogsStorage(this, appChatsManager, appPeersManager, serverTimeManager);
    this.filtersStorage = new FiltersStorage(appPeersManager, appUsersManager, /* apiManager, */ rootScope);

    rootScope.on('apiUpdate', (e) => {
      this.handleUpdate(e);
    });

    // ! Invalidate notify settings, can optimize though
    rootScope.on('notify_peer_type_settings', ({key, settings}) => {
      this.getConversationsAll().then(dialogs => {
        let filterFunc: (dialog: Dialog) => boolean;
        if(key === 'notifyUsers') filterFunc = (dialog) => dialog.peerId > 0;
        else if(key === 'notifyBroadcasts') filterFunc = (dialog) => appChatsManager.isBroadcast(-dialog.peerId);
        else filterFunc = (dialog) => appPeersManager.isAnyGroup(dialog.peerId);

        dialogs
        .filter(filterFunc)
        .forEach(dialog => {
          rootScope.broadcast('dialog_notify_settings', dialog);
        });
      });
    });

    rootScope.on('webpage_updated', (e) => {
      const eventData = e;
      eventData.msgs.forEach((mid) => {
        const message = this.getMessageById(mid) as Message.message;
        if(!message) return;
        message.media = {
          _: 'messageMediaWebPage', 
          webpage: appWebPagesManager.getWebPage(eventData.id)
        };

        const peerId = this.getMessagePeer(message);
        const storage = this.getMessagesStorage(peerId);
        rootScope.broadcast('message_edit', {
          storage,
          peerId,
          mid
        });
      });
    });

    rootScope.on('draft_updated', (e) => {
      const {peerId, threadId, draft} = e;

      if(threadId) return;

      const dialog = this.getDialogByPeerId(peerId)[0];
      if(dialog && !threadId) {
        dialog.draft = draft;
        this.dialogsStorage.generateIndexForDialog(dialog);
        this.dialogsStorage.pushDialog(dialog);

        rootScope.broadcast('dialog_draft', {
          peerId,
          draft,
          index: dialog.index
        });
      } else {
        this.reloadConversation(peerId);
      }
    });

    appStateManager.getState().then(state => {
      if(state.maxSeenMsgId) {
        this.maxSeenId = state.maxSeenMsgId;
      }

      const messages = state.messages;
      if(messages) {
        /* let tempId = this.tempId;

        for(let message of messages) {
          if(message.id < tempId) {
            tempId = message.id;
          }
        }

        if(tempId !== this.tempId) {
          this.log('Set tempId to:', tempId);
          this.tempId = tempId;
        } */

        this.saveMessages(messages);
      }

      if(!state.dialogs || !Object.keys(state.dialogs).length) {
        state.allDialogsLoaded = {};
      }
      
      if(state.allDialogsLoaded) {
        this.dialogsStorage.allDialogsLoaded = state.allDialogsLoaded;
      }

      if(state.filters) {
        for(const filterId in state.filters) {
          this.filtersStorage.saveDialogFilter(state.filters[filterId], false);
        }
      }

      if(state.dialogs) {
        forEachReverse(state.dialogs, dialog => {
          dialog.top_message = this.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog

          this.saveConversation(dialog);

          // ! WARNING, убрать это когда нужно будет делать чтобы pending сообщения сохранялись
          const message = this.getMessageByPeer(dialog.peerId, dialog.top_message);
          if(message.deleted) {
            this.reloadConversation(dialog.peerId);
          }
        });
      }

      appStateManager.addEventListener('save', this.saveState);
    });

    appNotificationsManager.start();
  }

  private saveState = () => {
    const messages: any[] = [];
    const dialogs: Dialog[] = [];
    const items: any[] = [];
    
    const processDialog = (dialog: MTDialog.dialog) => {
      const historyStorage = this.getHistoryStorage(dialog.peerId);
      const history = [].concat(historyStorage.history.slice);
      //dialog = copy(dialog);
      let removeUnread = 0;
      for(const mid of history) {
        const message = this.getMessageByPeer(dialog.peerId, mid);
        if(/* message._ !== 'messageEmpty' &&  */!message.pFlags.is_outgoing) {
          messages.push(message);
    
          if(message.fromId !== dialog.peerId) {
            appStateManager.setPeer(message.fromId, appPeersManager.getPeer(message.fromId));
          }
  
          /* dialog.top_message = message.mid;
          this.dialogsStorage.generateIndexForDialog(dialog, false, message); */
  
          break;
        } else if(message.pFlags && message.pFlags.unread) {
          ++removeUnread;
        }
      }
  
      if(removeUnread && dialog.unread_count) dialog.unread_count -= removeUnread;

      if(dialog.peerId < 0 && dialog.pts) {
        const newPts = apiUpdatesManager.channelStates[-dialog.peerId].pts;
        dialog.pts = newPts;
      }
  
      dialog.unread_count = Math.max(0, dialog.unread_count);
      dialogs.push(dialog);
  
      appStateManager.setPeer(dialog.peerId, appPeersManager.getPeer(dialog.peerId));
    };

    for(const folderId in this.dialogsStorage.byFolders) {
      const folder = this.dialogsStorage.getFolder(+folderId);
      
      for(let dialog of folder) {
        items.push([dialog]);
      }
    }

    return pushHeavyTask({
      items, 
      process: processDialog, 
      context: this
    }).then(() => {
      appStateManager.pushToState('dialogs', dialogs);
      appStateManager.pushToState('messages', messages);
      appStateManager.pushToState('filters', this.filtersStorage.filters);
      appStateManager.pushToState('allDialogsLoaded', this.dialogsStorage.allDialogsLoaded);
      appStateManager.pushToState('maxSeenMsgId', this.maxSeenId);
    });
  };

  public getInputEntities(entities: MessageEntity[]) {
    var sendEntites = copy(entities);
    sendEntites.forEach((entity: any) => {
      if(entity._ === 'messageEntityMentionName') {
        entity._ = 'inputMessageEntityMentionName';
        entity.user_id = appUsersManager.getUserInput(entity.user_id);
      }
    });
    return sendEntites;
  }

  public invokeAfterMessageIsSent(tempId: number, callbackName: string, callback: (message: any) => Promise<any>) {
    const finalize = this.tempFinalizeCallbacks[tempId] ?? (this.tempFinalizeCallbacks[tempId] = {});
    const obj = finalize[callbackName] ?? (finalize[callbackName] = {deferred: deferredPromise<void>()});

    obj.callback = callback;

    return obj.deferred;
  }

  public editMessage(message: any, text: string, options: Partial<{
    noWebPage: true,
    newMedia: any,
    scheduleDate: number,
    entities: MessageEntity[]
  }> = {}): Promise<void> {
    /* if(!this.canEditMessage(messageId)) {
      return Promise.reject({type: 'MESSAGE_EDIT_FORBIDDEN'});
    } */

    const {mid, peerId} = message;

    if(message.pFlags.is_outgoing) {
      return this.invokeAfterMessageIsSent(mid, 'edit', (message) => {
        //this.log('invoke editMessage callback', message);
        return this.editMessage(message, text, options);
      });
    }

    let entities = options.entities || [];
    if(text) {
      text = RichTextProcessor.parseMarkdown(text, entities);
    }

    const schedule_date = options.scheduleDate || (message.pFlags.is_scheduled ? message.date : undefined);
    return apiManager.invokeApi('messages.editMessage', {
      peer: appPeersManager.getInputPeerById(peerId),
      id: message.id,
      message: text,
      media: options.newMedia,
      entities: entities.length ? this.getInputEntities(entities) : undefined,
      no_webpage: options.noWebPage,
      schedule_date
    }).then((updates) => {
      apiUpdatesManager.processUpdateMessage(updates);
    }, (error) => {
      this.log.error('editMessage error:', error);
      
      if(error && error.type === 'MESSAGE_NOT_MODIFIED') {
        error.handled = true;
        return;
      }
      if(error && error.type === 'MESSAGE_EMPTY') {
        error.handled = true;
      }
      return Promise.reject(error);
    });
  }

  public sendText(peerId: number, text: string, options: Partial<{
    entities: any[],
    replyToMsgId: number,
    threadId: number,
    viaBotId: number,
    queryId: string,
    resultId: string,
    noWebPage: true,
    reply_markup: any,
    clearDraft: true,
    webPage: any,
    scheduleDate: number,
    silent: true
  }> = {}) {
    if(typeof(text) !== 'string' || !text.length) {
      return;
    }

    //this.checkSendOptions(options);

    if(options.threadId && !options.replyToMsgId) {
      options.replyToMsgId = options.threadId;
    }

    const MAX_LENGTH = 4096;
    if(text.length > MAX_LENGTH) {
      const splitted = splitStringByLength(text, MAX_LENGTH);
      text = splitted[0];

      if(splitted.length > 1) {
        delete options.webPage;
      }

      for(let i = 1; i < splitted.length; ++i) {
        setTimeout(() => {
          this.sendText(peerId, splitted[i], options);
        }, i);
      }
    }

    peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;

    let entities = options.entities || [];
    if(!options.viaBotId) {
      text = RichTextProcessor.parseMarkdown(text, entities);
    }

    let sendEntites = this.getInputEntities(entities);
    if(!sendEntites.length) {
      sendEntites = undefined;
    }

    const message = this.generateOutgoingMessage(peerId, options);
    message.entities = entities;
    message.message = text;

    const replyToMsgId = options.replyToMsgId ? this.getServerMessageId(options.replyToMsgId) : undefined;
    const isChannel = appPeersManager.isChannel(peerId);

    if(options.webPage) {
      message.media = {
        _: 'messageMediaWebPage',
        webpage: options.webPage
      };
    }

    const toggleError = (on: any) => {
      if(on) {
        message.error = true;
      } else {
        delete message.error;
      }
      rootScope.broadcast('messages_pending');
    };

    message.send = () => {
      toggleError(false);
      const sentRequestOptions: InvokeApiOptions = {};
      if(this.pendingAfterMsgs[peerId]) {
        sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId;
      }

      let apiPromise: any;
      if(options.viaBotId) {
        apiPromise = apiManager.invokeApiAfter('messages.sendInlineBotResult', {
          peer: appPeersManager.getInputPeerById(peerId),
          random_id: message.random_id,
          reply_to_msg_id: replyToMsgId || undefined,
          query_id: options.queryId,
          id: options.resultId,
          clear_draft: options.clearDraft
        }, sentRequestOptions);
      } else {
        apiPromise = apiManager.invokeApiAfter('messages.sendMessage', {
          no_webpage: options.noWebPage,
          peer: appPeersManager.getInputPeerById(peerId),
          message: text,
          random_id: message.random_id,
          reply_to_msg_id: replyToMsgId || undefined,
          entities: sendEntites,
          clear_draft: options.clearDraft,
          schedule_date: options.scheduleDate || undefined,
          silent: options.silent
        }, sentRequestOptions);
      }

      //this.log('sendText', message.mid);
      apiPromise.then((updates: any) => {
        //this.log('sendText sent', message.mid);
        if(updates._ === 'updateShortSentMessage') {
          message.date = updates.date;
          message.id = updates.id;
          message.media = updates.media;
          message.entities = updates.entities;

          // * override with new updates
          updates = {
            _: 'updates',
            users: [],
            chats: [],
            seq: 0,
            updates: [{
              _: 'updateMessageID',
              random_id: message.random_id,
              id: updates.id
            }, {
              _: options.scheduleDate ? 'updateNewScheduledMessage' : (isChannel ? 'updateNewChannelMessage' : 'updateNewMessage'),
              message: message,
              pts: updates.pts,
              pts_count: updates.pts_count
            }]
          };
        } else if(updates.updates) {
          updates.updates.forEach((update: any) => {
            if(update._ === 'updateDraftMessage') {
              update.local = true;
            }
          });
        }
        // Testing bad situations
        // var upd = angular.copy(updates)
        // updates.updates.splice(0, 1)

        apiUpdatesManager.processUpdateMessage(updates);

        // $timeout(function () {
        // ApiUpdatesManager.processUpdateMessage(upd)
        // }, 5000)
      }, (/* error: any */) => {
        toggleError(true);
      }).finally(() => {
        if(this.pendingAfterMsgs[peerId] === sentRequestOptions) {
          delete this.pendingAfterMsgs[peerId];
        }
      });

      this.pendingAfterMsgs[peerId] = sentRequestOptions;
    }

    this.beforeMessageSending(message, {
      isScheduled: !!options.scheduleDate || undefined, 
      threadId: options.threadId,
      clearDraft: options.clearDraft
    });
  }

  public sendFile(peerId: number, file: File | Blob | MyDocument, options: Partial<{
    isRoundMessage: true,
    isVoiceMessage: true,
    isGroupedItem: true,
    isMedia: true,

    replyToMsgId: number,
    threadId: number,
    groupId: string,
    caption: string,
    entities: MessageEntity[],
    width: number,
    height: number,
    objectURL: string,
    thumbBlob: Blob,
    thumbURL: string,
    duration: number,
    background: true,
    silent: true,
    clearDraft: true,
    scheduleDate: number,

    waveform: Uint8Array,
  }> = {}) {
    peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;

    //this.checkSendOptions(options);

    const message = this.generateOutgoingMessage(peerId, options);
    const replyToMsgId = options.replyToMsgId ? this.getServerMessageId(options.replyToMsgId) : undefined;

    let attachType: string, apiFileName: string;

    const fileType = 'mime_type' in file ? file.mime_type : file.type;
    const fileName = file instanceof File ? file.name : '';
    const isDocument = !(file instanceof File) && !(file instanceof Blob);
    let caption = options.caption || '';

    this.log('sendFile', file, fileType);

    const entities = options.entities || [];
    if(caption) {
      caption = RichTextProcessor.parseMarkdown(caption, entities);
    }

    const attributes: DocumentAttribute[] = [];

    const isPhoto = ['image/jpeg', 'image/png', 'image/bmp'].indexOf(fileType) >= 0;

    let photo: MyPhoto, document: MyDocument;

    let actionName = '';
    if(isDocument) { // maybe it's a sticker or gif
      attachType = 'document';
      apiFileName = '';
    } else if(fileType.indexOf('audio/') === 0 || ['video/ogg'].indexOf(fileType) >= 0) {
      attachType = 'audio';
      apiFileName = 'audio.' + (fileType.split('/')[1] === 'ogg' ? 'ogg' : 'mp3');
      actionName = 'sendMessageUploadAudioAction';

      if(options.isVoiceMessage) {
        attachType = 'voice';
        message.pFlags.media_unread = true;
      }

      let attribute: DocumentAttribute.documentAttributeAudio = {
        _: 'documentAttributeAudio',
        pFlags: {
          voice: options.isVoiceMessage
        },
        waveform: options.waveform,
        duration: options.duration || 0
      };

      attributes.push(attribute);
    } else if(!options.isMedia) {
      attachType = 'document';
      apiFileName = 'document.' + fileType.split('/')[1];
      actionName = 'sendMessageUploadDocumentAction';
    } else if(isPhoto) {
      attachType = 'photo';
      apiFileName = 'photo.' + fileType.split('/')[1];
      actionName = 'sendMessageUploadPhotoAction';

      photo = {
        _: 'photo',
        id: '' + message.id,
        sizes: [{
          _: 'photoSize',
          w: options.width,
          h: options.height,
          type: 'full',
          location: null,
          size: file.size
        }],
        w: options.width,
        h: options.height
      } as any;

      defineNotNumerableProperties(photo, ['downloaded', 'url']);
      photo.downloaded = file.size;
      photo.url = options.objectURL || '';
      
      appPhotosManager.savePhoto(photo);
    } else if(fileType.indexOf('video/') === 0) {
      attachType = 'video';
      apiFileName = 'video.mp4';
      actionName = 'sendMessageUploadVideoAction';

      let videoAttribute: DocumentAttribute.documentAttributeVideo = {
        _: 'documentAttributeVideo',
        pFlags: {
          round_message: options.isRoundMessage
        }, 
        duration: options.duration,
        w: options.width,
        h: options.height
      };

      attributes.push(videoAttribute);
    } else {
      attachType = 'document';
      apiFileName = 'document.' + fileType.split('/')[1];
      actionName = 'sendMessageUploadDocumentAction';
    }

    attributes.push({_: 'documentAttributeFilename', file_name: fileName || apiFileName});

    if(['document', 'video', 'audio', 'voice'].indexOf(attachType) !== -1 && !isDocument) {
      const thumbs: PhotoSize[] = [];
      document = {
        _: 'document',
        id: '' + message.id,
        duration: options.duration,
        attributes,
        w: options.width,
        h: options.height,
        thumbs,
        mime_type: fileType,
        size: file.size
      } as any;

      defineNotNumerableProperties(document, ['downloaded', 'url']);
      // @ts-ignore
      document.downloaded = file.size;
      document.url = options.objectURL || '';

      if(isPhoto) {
        attributes.push({
          _: 'documentAttributeImageSize',
          w: options.width,
          h: options.height
        });

        thumbs.push({
          _: 'photoSize',
          w: options.width,
          h: options.height,
          type: 'full',
          location: null,
          size: file.size,
          url: options.objectURL
        });
      } else if(attachType === 'video') {
        if(options.thumbURL) {
          thumbs.push({
            _: 'photoSize',
            w: options.width,
            h: options.height,
            type: 'full',
            location: null,
            size: options.thumbBlob.size,
            url: options.thumbURL
          });
        }

        const thumb = thumbs[0] as PhotoSize.photoSize;
        const docThumb = appPhotosManager.getDocumentCachedThumb(document.id);
        docThumb.downloaded = thumb.size;
        docThumb.url = thumb.url;
      }

      /* if(thumbs.length) {
        const thumb = thumbs[0] as PhotoSize.photoSize;
        const docThumb = appPhotosManager.getDocumentCachedThumb(document.id);
        docThumb.downloaded = thumb.size;
        docThumb.url = thumb.url;
      } */
      
      appDocsManager.saveDoc(document);
    }

    this.log('sendFile', attachType, apiFileName, file.type, options);

    const preloader = isDocument ? undefined : new ProgressivePreloader({
      attachMethod: 'prepend',
      tryAgainOnFail: false,
      isUpload: true
    });

    const sentDeferred = deferredPromise<InputMedia>();

    if(preloader) {
      preloader.attachPromise(sentDeferred);
      sentDeferred.cancel = () => {
        const error = new Error('Download canceled');
        error.name = 'AbortError';
        sentDeferred.reject(error);
      };

      sentDeferred.catch(err => {
        if(err.name === 'AbortError' && !uploaded) {
          this.log('cancelling upload', media);

          sentDeferred.reject(err);
          this.cancelPendingMessage(message.random_id);
          this.setTyping(peerId, 'sendMessageCancelAction');

          if(uploadPromise?.cancel) {
            uploadPromise.cancel();
          }
        }
      });
    }

    const media = isDocument ? undefined : {
      _: photo ? 'messageMediaPhoto' : 'messageMediaDocument',
      pFlags: {},
      preloader,
      photo,
      document,
      promise: sentDeferred
    };

    message.entities = entities;
    message.message = caption;
    message.media = isDocument ? {
      _: 'messageMediaDocument',
      pFlags: {},
      document: file 
    } : media;

    const toggleError = (on: boolean) => {
      if(on) {
        message.error = true;
      } else {
        delete message.error;
      }

      rootScope.broadcast('messages_pending');
    };

    let uploaded = false,
      uploadPromise: ReturnType<ApiFileManager['uploadFile']> = null;

    message.send = () => {
      if(isDocument) {
        const {id, access_hash, file_reference} = file as MyDocument;

        const inputMedia: InputMedia = {
          _: 'inputMediaDocument',
          id: {
            _: 'inputDocument',
            id,
            access_hash,
            file_reference
          }
        };
        
        sentDeferred.resolve(inputMedia);
      } else if(file instanceof File || file instanceof Blob) {
        const load = () => {
          if(!uploaded || message.error) {
            uploaded = false;
            uploadPromise = appDownloadManager.upload(file);
            sentDeferred.notifyAll({done: 0, total: file.size});
          }

          let thumbUploadPromise: typeof uploadPromise;
          if(attachType === 'video' && options.objectURL) {
            thumbUploadPromise = new Promise((resolve, reject) => {
              const blobPromise = options.thumbBlob ? Promise.resolve(options.thumbBlob) : createPosterForVideo(options.objectURL);
              blobPromise.then(blob => {
                if(!blob) {
                  resolve(null);
                } else {
                  appDownloadManager.upload(blob).then(resolve, reject);
                }
              }, reject);
            });
          }
  
          uploadPromise && uploadPromise.then(async(inputFile) => {
            /* if(DEBUG) {
              this.log('appMessagesManager: sendFile uploaded:', inputFile);
            } */

            delete message.media.preloader;

            inputFile.name = apiFileName;
            uploaded = true;
            let inputMedia: InputMedia;
            switch(attachType) {
              case 'photo':
                inputMedia = {
                  _: 'inputMediaUploadedPhoto', 
                  file: inputFile
                };
                break;

              default:
                inputMedia = {
                  _: 'inputMediaUploadedDocument', 
                  file: inputFile, 
                  mime_type: fileType, 
                  attributes
                };
            }

            if(thumbUploadPromise) {
              try {
                const inputFile = await thumbUploadPromise;
                (inputMedia as InputMedia.inputMediaUploadedDocument).thumb = inputFile;
              } catch(err) {
                this.log.error('sendFile thumb upload error:', err);
              }
            }
            
            sentDeferred.resolve(inputMedia);
          }, (/* error */) => {
            toggleError(true);
          });
  
          uploadPromise.addNotifyListener((progress: {done: number, total: number}) => {
            /* if(DEBUG) {
              this.log('upload progress', progress);
            } */

            const percents = Math.max(1, Math.floor(100 * progress.done / progress.total));
            this.setTyping(peerId, {_: actionName, progress: percents | 0});
            sentDeferred.notifyAll(progress);
          });

          return sentDeferred;
        };

        if(options.isGroupedItem) {
          load();
        } else {
          this.sendSmthLazyLoadQueue.push({
            load
          });
        }
      }

      return sentDeferred;
    };

    this.beforeMessageSending(message, {
      isGroupedItem: options.isGroupedItem, 
      isScheduled: !!options.scheduleDate || undefined, 
      threadId: options.threadId,
      clearDraft: options.clearDraft
    });

    if(!options.isGroupedItem) {
      sentDeferred.then(inputMedia => {
        this.setTyping(peerId, 'sendMessageCancelAction');

        return apiManager.invokeApi('messages.sendMedia', {
          background: options.background,
          peer: appPeersManager.getInputPeerById(peerId),
          media: inputMedia,
          message: caption,
          random_id: message.random_id,
          reply_to_msg_id: replyToMsgId,
          schedule_date: options.scheduleDate,
          silent: options.silent,
          entities,
          clear_draft: options.clearDraft
        }).then((updates) => {
          apiUpdatesManager.processUpdateMessage(updates);
        }, (error) => {
          if(attachType === 'photo' &&
            error.code === 400 &&
            (error.type === 'PHOTO_INVALID_DIMENSIONS' ||
            error.type === 'PHOTO_SAVE_FILE_INVALID')) {
            error.handled = true;
            attachType = 'document';
            message.send();
            return;
          }

          toggleError(true);
        });
      });
    }

    return {message, promise: sentDeferred};
  }

  public async sendAlbum(peerId: number, files: File[], options: Partial<{
    isMedia: true,
    entities: MessageEntity[],
    replyToMsgId: number,
    threadId: number,
    caption: string,
    sendFileDetails: Partial<{
      duration: number,
      width: number,
      height: number,
      objectURL: string,
      thumbBlob: Blob,
      thumbURL: string
    }>[],
    silent: true,
    clearDraft: true,
    scheduleDate: number
  }> = {}) {
    //this.checkSendOptions(options);

    if(options.threadId && !options.replyToMsgId) {
      options.replyToMsgId = options.threadId;
    }

    if(files.length === 1) {
      return this.sendFile(peerId, files[0], {...options, ...options.sendFileDetails[0]});
    }

    peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;
    const replyToMsgId = options.replyToMsgId ? this.getServerMessageId(options.replyToMsgId) : undefined;

    let caption = options.caption || '';
    let entities = options.entities || [];
    if(caption) {
      caption = RichTextProcessor.parseMarkdown(caption, entities);
    }

    this.log('sendAlbum', files, options);

    const groupId = '' + ++this.groupedTempId;

    const messages = files.map((file, idx) => {
      const details = options.sendFileDetails[idx];
      const o: any = {
        isGroupedItem: true,
        isMedia: options.isMedia,
        scheduleDate: options.scheduleDate,
        silent: options.silent,
        replyToMsgId,
        threadId: options.threadId,
        groupId,
        ...details
      };

      if(idx === 0) {
        o.caption = caption;
        o.entities = entities;
        //o.replyToMsgId = replyToMsgId;
      }

      return this.sendFile(peerId, file, o).message;
    });

    if(options.clearDraft) {
      appDraftsManager.syncDraft(peerId, options.threadId);
    }
    
    // * test pending
    //return;

    const toggleError = (message: any, on: boolean) => {
      if(on) {
        message.error = true;
      } else {
        delete message.error;
      }

      rootScope.broadcast('messages_pending');
    };

    const inputPeer = appPeersManager.getInputPeerById(peerId);
    const invoke = (multiMedia: any[]) => {
      this.setTyping(peerId, 'sendMessageCancelAction');

      this.sendSmthLazyLoadQueue.push({
        load: () => {
          return apiManager.invokeApi('messages.sendMultiMedia', {
            peer: inputPeer,
            multi_media: multiMedia,
            reply_to_msg_id: replyToMsgId,
            schedule_date: options.scheduleDate,
            silent: options.silent,
            clear_draft: options.clearDraft
          }).then((updates) => {
            apiUpdatesManager.processUpdateMessage(updates);
          }, (error) => {
            messages.forEach(message => toggleError(message, true));
          });
        }
      });
    };

    const promises: Promise<InputSingleMedia>[] = messages.map((message, idx) => {
      return (message.send() as Promise<InputMedia>).then((inputMedia: InputMedia) => {
        return apiManager.invokeApi('messages.uploadMedia', {
          peer: inputPeer,
          media: inputMedia
        });
      })
      .then(messageMedia => {
        let inputMedia: any;
        if(messageMedia._ === 'messageMediaPhoto') {
          const photo = appPhotosManager.savePhoto(messageMedia.photo);
          inputMedia = appPhotosManager.getInput(photo);
        } else if(messageMedia._ === 'messageMediaDocument') {
          const doc = appDocsManager.saveDoc(messageMedia.document);
          inputMedia = appDocsManager.getMediaInput(doc);
        }

        const inputSingleMedia: InputSingleMedia = {
          _: 'inputSingleMedia',
          media: inputMedia,
          random_id: message.random_id,
          message: caption,
          entities
        };

        // * only 1 caption for all inputs
        if(caption) {
          caption = '';
          entities = [];
        }

        return inputSingleMedia;
      }).catch((err: any) => {
        if(err.name === 'AbortError') {
          return null;
        }

        this.log.error('sendAlbum upload item error:', err, message);
        toggleError(message, true);
        throw err;
      });
    });

    Promise.all(promises).then(inputs => {
      invoke(inputs.filter(Boolean));
    });
  }

  public sendOther(peerId: number, inputMedia: any, options: Partial<{
    replyToMsgId: number,
    threadId: number,
    viaBotId: number,
    reply_markup: any,
    clearDraft: true,
    queryId: string
    resultId: string,
    scheduleDate: number,
    silent: true
  }> = {}) {
    peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;

    //this.checkSendOptions(options);
    const message = this.generateOutgoingMessage(peerId, options);
    const replyToMsgId = options.replyToMsgId ? this.getServerMessageId(options.replyToMsgId) : undefined;

    let media;
    switch(inputMedia._) {
      case 'inputMediaPoll': {
        inputMedia.poll.id = message.id;
        appPollsManager.savePoll(inputMedia.poll, {
          _: 'pollResults',
          flags: 4,
          total_voters: 0,
          pFlags: {},
        });

        const {poll, results} = appPollsManager.getPoll('' + message.id);
        media = {
          _: 'messageMediaPoll',
          poll,
          results
        };

        break;
      }
      /* case 'inputMediaPhoto':
        media = {
          _: 'messageMediaPhoto',
          photo: appPhotosManager.getPhoto(inputMedia.id.id),
          caption: inputMedia.caption || ''
        };
        break;

      case 'inputMediaDocument':
        var doc = appDocsManager.getDoc(inputMedia.id.id);
        if(doc.sticker && doc.stickerSetInput) {
          appStickersManager.pushPopularSticker(doc.id);
        }
        media = {
          _: 'messageMediaDocument',
          'document': doc,
          caption: inputMedia.caption || ''
        };
        break;

      case 'inputMediaContact':
        media = {
          _: 'messageMediaContact',
          phone_number: inputMedia.phone_number,
          first_name: inputMedia.first_name,
          last_name: inputMedia.last_name,
          user_id: 0
        };
        break;

      case 'inputMediaGeoPoint':
        media = {
          _: 'messageMediaGeo',
          geo: {
            _: 'geoPoint',
            'lat': inputMedia.geo_point['lat'],
            'long': inputMedia.geo_point['long']
          }
        };
        break;

      case 'inputMediaVenue':
        media = {
          _: 'messageMediaVenue',
          geo: {
            _: 'geoPoint',
            'lat': inputMedia.geo_point['lat'],
            'long': inputMedia.geo_point['long']
          },
          title: inputMedia.title,
          address: inputMedia.address,
          provider: inputMedia.provider,
          venue_id: inputMedia.venue_id
        };
        break;

      case 'messageMediaPending':
        media = inputMedia;
        break; */
    }

    message.media = media;

    let toggleError = (on: boolean) => {
      /* const historyMessage = this.messagesForHistory[messageId];
      if (on) {
        message.error = true
        if (historyMessage) {
          historyMessage.error = true
        }
      } else {
        delete message.error
        if (historyMessage) {
          delete historyMessage.error
        }
      } */
      rootScope.broadcast('messages_pending');
    };

    message.send = () => {
      const sentRequestOptions: any = {};
      if(this.pendingAfterMsgs[peerId]) {
        sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId;
      }

      let apiPromise: Promise<any>;
      if(options.viaBotId) {
        apiPromise = apiManager.invokeApiAfter('messages.sendInlineBotResult', {
          peer: appPeersManager.getInputPeerById(peerId),
          random_id: message.random_id,
          reply_to_msg_id: replyToMsgId || undefined,
          query_id: options.queryId,
          id: options.resultId,
          clear_draft: options.clearDraft
        }, sentRequestOptions);
      } else {
        apiPromise = apiManager.invokeApiAfter('messages.sendMedia', {
          peer: appPeersManager.getInputPeerById(peerId),
          media: inputMedia,
          random_id: message.random_id,
          reply_to_msg_id: replyToMsgId || undefined,
          message: '',
          clear_draft: options.clearDraft,
          schedule_date: options.scheduleDate,
          silent: options.silent
        }, sentRequestOptions);
      }

      apiPromise.then((updates) => {
        if(updates.updates) {
          updates.updates.forEach((update: any) => {
            if(update._ === 'updateDraftMessage') {
              update.local = true
            }
          });
        }

        apiUpdatesManager.processUpdateMessage(updates);
      }, (error) => {
        toggleError(true);
      }).finally(() => {
        if(this.pendingAfterMsgs[peerId] === sentRequestOptions) {
          delete this.pendingAfterMsgs[peerId];
        }
      });
      this.pendingAfterMsgs[peerId] = sentRequestOptions;
    }

    this.beforeMessageSending(message, {
      isScheduled: !!options.scheduleDate || undefined, 
      threadId: options.threadId,
      clearDraft: options.clearDraft
    });
  }

  /* private checkSendOptions(options: Partial<{
    scheduleDate: number
  }>) {
    if(options.scheduleDate) {
      const minTimestamp = (Date.now() / 1000 | 0) + 10;
      if(options.scheduleDate <= minTimestamp) {
        delete options.scheduleDate;
      }
    }
  } */

  private beforeMessageSending(message: any, options: Partial<{
    isGroupedItem: true, 
    isScheduled: true, 
    threadId: number, 
    clearDraft: true
  }> = {}) {
    const messageId = message.id;
    const peerId = this.getMessagePeer(message);
    const storage = options.isScheduled ? this.getScheduledMessagesStorage(peerId) : this.getMessagesStorage(peerId);

    if(options.isScheduled) {
      //if(!options.isGroupedItem) {
      this.saveMessages([message], {storage, isScheduled: true, isOutgoing: true});
      setTimeout(() => {
        rootScope.broadcast('scheduled_new', {peerId, mid: messageId});
      }, 0);
    } else {
      if(options.threadId && this.threadsStorage[peerId]) {
        delete this.threadsStorage[peerId][options.threadId];
      }
      //if(options.threadId) {
        const historyStorage = this.getHistoryStorage(peerId/* , options.threadId */);
        historyStorage.history.unshift(messageId);
      //}

      /* const historyStorage = this.getHistoryStorage(peerId);
      historyStorage.history.unshift(messageId); */

      //if(!options.isGroupedItem) {
      this.saveMessages([message], {storage, isOutgoing: true});
      setTimeout(() => {
        this.setDialogTopMessage(message);
        rootScope.broadcast('history_append', {peerId, messageId, my: true});
      }, 0);
    }

    if(!options.isGroupedItem && options.clearDraft && !options.threadId) {
      appDraftsManager.syncDraft(peerId, options.threadId);
    }
    
    this.pendingByRandomId[message.random_id] = {peerId, tempId: messageId, storage};

    if(!options.isGroupedItem && message.send) {
      setTimeout(message.send, 0);
      //setTimeout(message.send, 4000);
      //setTimeout(message.send, 7000);
    }
  }

  private generateOutgoingMessage(peerId: number, options: Partial<{
    scheduleDate: number,
    replyToMsgId: number,
    threadId: number,
    viaBotId: number,
    groupId: string,
    reply_markup: any,
  }>) {
    if(options.threadId && !options.replyToMsgId) {
      options.replyToMsgId = options.threadId;
    }

    const message: any = {
      _: 'message',
      id: this.generateTempMessageId(peerId),
      from_id: this.generateFromId(peerId),
      peer_id: appPeersManager.getOutputPeer(peerId),
      pFlags: this.generateFlags(peerId),
      date: options.scheduleDate || (tsNow(true) + serverTimeManager.serverTimeOffset),
      message: '',
      grouped_id: options.groupId,
      random_id: randomLong(),
      reply_to: this.generateReplyHeader(options.replyToMsgId, options.threadId),
      via_bot_id: options.viaBotId,
      reply_markup: options.reply_markup,
      replies: this.generateReplies(peerId),
      views: appPeersManager.isBroadcast(peerId) && 1,
      pending: true,
    };

    return message;
  }

  private generateReplyHeader(replyToMsgId: number, replyToTopId?: number) {
    const header = {
      _: 'messageReplyHeader',
      reply_to_msg_id: replyToMsgId || replyToTopId,
    } as MessageReplyHeader;

    if(replyToTopId && header.reply_to_msg_id !== replyToTopId) {
      header.reply_to_top_id = replyToTopId;
    }

    return header;
  }

  private generateReplies(peerId: number) {
    let replies: MessageReplies.messageReplies;
    if(appPeersManager.isBroadcast(peerId)) {
      const channelFull = appProfileManager.chatsFull[-peerId] as ChatFull.channelFull;
      if(channelFull?.linked_chat_id) {
        replies = {
          _: 'messageReplies',
          flags: 1,
          pFlags: {
            comments: true
          },
          channel_id: channelFull.linked_chat_id,
          replies: 0,
          replies_pts: 0
        };
      }
    }

    return replies;
  }

  /**
   * Generate correct from_id according to anonymous or broadcast
   */
  private generateFromId(peerId: number) {
    if(peerId < 0 && (appPeersManager.isBroadcast(peerId) || appPeersManager.getPeer(peerId).admin_rights?.pFlags?.anonymous)) {
      return undefined;
    } else {
      return appPeersManager.getOutputPeer(appUsersManager.getSelf().id);
    }
  }

  private generateFlags(peerId: number) {
    const pFlags: any = {};
    const fromId = appUsersManager.getSelf().id;
    if(peerId !== fromId) {
      pFlags.out = true;

      if(!appPeersManager.isChannel(peerId) && !appUsersManager.isBot(peerId)) {
        pFlags.unread = true;
      }
    }

    if(appPeersManager.isBroadcast(peerId)) {
      pFlags.post = true;
    }

    return pFlags;
  }

  private generateForwardHeader(peerId: number, originalMessage: Message.message) {
    const myId = appUsersManager.getSelf().id;
    if(originalMessage.fromId === myId && originalMessage.peerId === myId && !originalMessage.fwd_from) {
      return;
    }

    const fwdHeader: MessageFwdHeader.messageFwdHeader = {
      _: 'messageFwdHeader',
      flags: 0,
      date: originalMessage.date
    };

    if(originalMessage.fwd_from) {
      fwdHeader.from_id = originalMessage.fwd_from.from_id;
      fwdHeader.from_name = originalMessage.fwd_from.from_name;
      fwdHeader.post_author = originalMessage.fwd_from.post_author;
    } else {
      fwdHeader.from_id = appPeersManager.getOutputPeer(originalMessage.fromId);
      fwdHeader.post_author = originalMessage.post_author;
    }

    if(appPeersManager.isBroadcast(originalMessage.peerId)) {
      if(originalMessage.post_author) {
        fwdHeader.post_author = originalMessage.post_author;
      }

      fwdHeader.channel_post = originalMessage.id;
    }
    
    // * there is no way to detect whether user profile is hidden
    if(peerId === myId) {
      fwdHeader.saved_from_msg_id = originalMessage.id;
      fwdHeader.saved_from_peer = appPeersManager.getOutputPeer(originalMessage.peerId);
    }

    return fwdHeader;
  }

  public setDialogTopMessage(message: MyMessage, dialog: MTDialog.dialog = this.getDialogByPeerId(message.peerId)[0]) {
    if(dialog) {
      dialog.top_message = message.mid;
      
      const historyStorage = this.getHistoryStorage(message.peerId);
      historyStorage.maxId = message.mid;

      this.dialogsStorage.generateIndexForDialog(dialog, false, message);

      this.newDialogsToHandle[message.peerId] = dialog;
      this.scheduleHandleNewDialogs();
    }
  }

  public cancelPendingMessage(randomId: string) {
    const pendingData = this.pendingByRandomId[randomId];

    /* if(DEBUG) {
      this.log('cancelPendingMessage', randomId, pendingData);
    } */

    if(pendingData) {
      const {peerId, tempId, storage} = pendingData;
      const historyStorage = this.getHistoryStorage(peerId);
      const pos = historyStorage.history.findSlice(tempId);

      apiUpdatesManager.processUpdateMessage({
        _: 'updateShort',
        update: {
          _: 'updateDeleteMessages',
          messages: [tempId]
        }
      });

      if(pos) {
        pos.slice.splice(pos.index, 1);
      }

      delete this.pendingByRandomId[randomId];
      delete storage[tempId];

      return true;
    }

    return false;
  }

  public async refreshConversations() {
    const limit = 100, outDialogs: Dialog[] = [];
    for(let folderId = 0; folderId < 2; ++folderId) {
      let offsetDate = 0;
      for(;;) {
        const {dialogs} = await appMessagesManager.getTopMessages(limit, folderId, offsetDate);
  
        if(dialogs.length) {
          outDialogs.push(...dialogs as Dialog[]);
          const dialog = dialogs[dialogs.length - 1];

          // * get peerId and mid manually, because dialog can be migrated peer and it won't be saved
          const peerId = appPeersManager.getPeerId(dialog.peer);
          const mid = this.generateMessageId(dialog.top_message);
          offsetDate = this.getMessageByPeer(peerId, mid).date;

          if(!offsetDate) {
            console.error('refreshConversations: got no offsetDate', dialog);
            break;
          }
        } else {
          break;
        }
      }
    }

    let obj: {[peerId: string]: Dialog} = {};
    outDialogs.forEach(dialog => {
      obj[dialog.peerId] = dialog;
    });
    rootScope.broadcast('dialogs_multiupdate', obj);

    return outDialogs;
  }

  public async getConversationsAll(query = '', folderId = 0) {
    const limit = 100, outDialogs: Dialog[] = [];
    for(; folderId < 2; ++folderId) {
      let offsetIndex = 0;
      for(;;) {
        const {dialogs} = await appMessagesManager.getConversations(query, offsetIndex, limit, folderId);
  
        if(dialogs.length) {
          outDialogs.push(...dialogs);
          offsetIndex = dialogs[dialogs.length - 1].index || 0;
        } else {
          break;
        }
      }
    }

    return outDialogs;
  }

  public getConversations(query = '', offsetIndex?: number, limit = 20, folderId = 0) {
    const realFolderId = folderId > 1 ? 0 : folderId;
    let curDialogStorage = this.dialogsStorage.getFolder(folderId);

    if(query) {
      if(!limit || this.cachedResults.query !== query || this.cachedResults.folderId !== folderId) {
        this.cachedResults.query = query;
        this.cachedResults.folderId = folderId;

        const results = searchIndexManager.search(query, this.dialogsIndex);

        this.cachedResults.dialogs = [];

        for(const peerId in this.dialogsStorage.dialogs) {
          const dialog = this.dialogsStorage.dialogs[peerId];
          if(results[dialog.peerId] && dialog.folder_id === folderId) {
            this.cachedResults.dialogs.push(dialog);
          }
        }

        this.cachedResults.dialogs.sort((d1, d2) => d2.index - d1.index);

        this.cachedResults.count = this.cachedResults.dialogs.length;
      }

      curDialogStorage = this.cachedResults.dialogs;
    } else {
      this.cachedResults.query = '';
    }

    let offset = 0;
    if(offsetIndex > 0) {
      for(; offset < curDialogStorage.length; offset++) {
        if(offsetIndex > curDialogStorage[offset].index) {
          break;
        }
      }
    }

    if(query || this.dialogsStorage.allDialogsLoaded[realFolderId] || curDialogStorage.length >= offset + limit) {
      return Promise.resolve({
        dialogs: curDialogStorage.slice(offset, offset + limit),
        count: this.dialogsStorage.allDialogsLoaded[realFolderId] ? curDialogStorage.length : null,
        isEnd: this.dialogsStorage.allDialogsLoaded[realFolderId] && (offset + limit) >= curDialogStorage.length
      });
    }

    return this.getTopMessages(limit, realFolderId).then(messagesDialogs => {
      //const curDialogStorage = this.dialogsStorage[folderId];

      offset = 0;
      if(offsetIndex > 0) {
        for(; offset < curDialogStorage.length; offset++) {
          if(offsetIndex > curDialogStorage[offset].index) {
            break;
          }
        }
      }

      //this.log.warn(offset, offset + limit, curDialogStorage.dialogs.length, this.dialogsStorage.dialogs.length);

      return {
        dialogs: curDialogStorage.slice(offset, offset + limit),
        count: messagesDialogs._ === 'messages.dialogs' ? messagesDialogs.dialogs.length : messagesDialogs.count,
        isEnd: this.dialogsStorage.allDialogsLoaded[realFolderId] && (offset + limit) >= curDialogStorage.length
      };
    });
  }

  public getReadMaxIdIfUnread(peerId: number, threadId?: number) {
    const historyStorage = this.getHistoryStorage(peerId, threadId);
    if(threadId) {
      const chatHistoryStorage = this.getHistoryStorage(peerId);
      const readMaxId = Math.max(chatHistoryStorage.readMaxId, historyStorage.readMaxId);
      const message = this.getMessageByPeer(peerId, historyStorage.maxId);
      return !message.pFlags.out && readMaxId < historyStorage.maxId ? readMaxId : 0;
    } else {
      const message = this.getMessageByPeer(peerId, historyStorage.maxId);
      const maxId = peerId > 0 ? Math.max(historyStorage.readMaxId, historyStorage.readOutboxMaxId) : historyStorage.readMaxId;
      return !message.pFlags.out && maxId < historyStorage.maxId ? maxId : 0;
    }
  }

  public getTopMessages(limit: number, folderId: number, offsetDate?: number) {
    const dialogs = this.dialogsStorage.getFolder(folderId);
    let offsetId = 0;
    let offsetPeerId = 0;
    let offsetIndex = 0;

    if(offsetDate === undefined) {
      offsetDate = this.dialogsStorage.getOffsetDate(folderId);
    }

    if(offsetDate) {
      offsetIndex = offsetDate * 0x10000;
      offsetDate += serverTimeManager.serverTimeOffset;
    }

    // ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА:
    // ! если делать запрос сначала по папке 0, потом по папке 1, по индексу 0 в массиве будет один и тот же диалог, с dialog.pFlags.pinned, ЛОЛ???
    // ! т.е., с запросом folder_id: 1, и exclude_pinned: 0, в результате будут ещё и закреплённые с папки 0
    return apiManager.invokeApi('messages.getDialogs', {
      folder_id: folderId,
      offset_date: offsetDate,
      offset_id: offsetId,
      offset_peer: appPeersManager.getInputPeerById(offsetPeerId),
      limit,
      hash: 0
    }, {
      //timeout: APITIMEOUT,
      noErrorBox: true
    }).then((dialogsResult) => {
      if(dialogsResult._ === 'messages.dialogsNotModified') return null;

      if(DEBUG) {
        this.log('messages.getDialogs result:', dialogsResult.dialogs, {...dialogsResult.dialogs[0]});
      }

      /* if(!offsetDate) {
        telegramMeWebService.setAuthorized(true);
      } */

      appUsersManager.saveApiUsers(dialogsResult.users);
      appChatsManager.saveApiChats(dialogsResult.chats);
      this.saveMessages(dialogsResult.messages);

      let maxSeenIdIncremented = offsetDate ? true : false;
      let hasPrepend = false;
      const noIdsDialogs: {[peerId: number]: Dialog} = {};
      forEachReverse((dialogsResult.dialogs as Dialog[]), dialog => {
        //const d = Object.assign({}, dialog);
        // ! нужно передавать folderId, так как по папке !== 0 нет свойства folder_id
        this.saveConversation(dialog, dialog.folder_id ?? folderId);

        if(dialog.peerId === undefined) {
          return;
        }

        /* if(dialog.peerId === -1213511294) {
          this.log.error('lun bot', folderId, d);
        } */

        if(offsetIndex && dialog.index > offsetIndex) {
          this.newDialogsToHandle[dialog.peerId] = dialog;
          hasPrepend = true;
        }

        // ! это может случиться, если запрос идёт не по папке 0, а по 1. почему-то read'ов нет
        // ! в итоге, чтобы получить 1 диалог, делается первый запрос по папке 0, потом запрос для архивных по папке 1, и потом ещё перезагрузка архивного диалога
        if(!this.getServerMessageId(dialog.read_inbox_max_id) && !this.getServerMessageId(dialog.read_outbox_max_id)) {
          noIdsDialogs[dialog.peerId] = dialog;

          this.log.error('noIdsDialogs', dialog);

          /* if(dialog.peerId === -1213511294) {
            this.log.error('lun bot', folderId);
          } */
        }

        if(!maxSeenIdIncremented &&
            !appPeersManager.isChannel(appPeersManager.getPeerId(dialog.peer))) {
          this.incrementMaxSeenId(dialog.top_message);
          maxSeenIdIncremented = true;
        }
      });

      if(Object.keys(noIdsDialogs).length) {
        //setTimeout(() => { // test bad situation
          this.reloadConversation(Object.keys(noIdsDialogs).map(id => +id)).then(() => {
            rootScope.broadcast('dialogs_multiupdate', noIdsDialogs);
  
            for(let peerId in noIdsDialogs) {
              rootScope.broadcast('dialog_unread', {peerId: +peerId});
            }
          });
        //}, 10e3);
      }

      const count = (dialogsResult as MessagesDialogs.messagesDialogsSlice).count;

      if(!dialogsResult.dialogs.length ||
        !count ||
        dialogs.length >= count) {
        this.dialogsStorage.allDialogsLoaded[folderId] = true;
      }

      if(hasPrepend) {
        this.scheduleHandleNewDialogs();
      } else {
        rootScope.broadcast('dialogs_multiupdate', {});
      }

      return dialogsResult;
    });
  }

  public forwardMessages(peerId: number, fromPeerId: number, mids: number[], options: Partial<{
    withMyScore: true,
    silent: true,
    scheduleDate: number
  }> = {}) {
    peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;
    mids = mids.slice().sort((a, b) => a - b);

    const groups: {
      [groupId: string]: {
        tempId: string,
        messages: any[]
      }
    } = {};

    const newMessages = mids.map(mid => {
      const originalMessage: Message.message = this.getMessageByPeer(fromPeerId, mid);
      const message: Message.message = this.generateOutgoingMessage(peerId, options);
      message.fwd_from = this.generateForwardHeader(peerId, originalMessage);

      (['entities', 'forwards', 'message', 'media', 'reply_markup', 'views'] as any as Array<keyof MyMessage>).forEach(key => {
        // @ts-ignore
        message[key] = originalMessage[key];
      });

      const document = (message.media as MessageMedia.messageMediaDocument)?.document as MyDocument;
      if(document) {
        const types: MyDocument['type'][] = ['round', 'voice'];
        if(types.includes(document.type)) {
          (message as MyMessage).pFlags.media_unread = true;
        }
      }

      if(originalMessage.grouped_id) {
        const group = groups[originalMessage.grouped_id] ?? (groups[originalMessage.grouped_id] = {tempId: '' + ++this.groupedTempId, messages: []});
        group.messages.push(message);
      }

      return message;
    });

    for(const groupId in groups) {
      const group = groups[groupId];
      if(group.messages.length > 1) {
        group.messages.forEach(message => {
          message.grouped_id = group.tempId;
        });
      }
    }

    newMessages.forEach(message => {
      this.beforeMessageSending(message, {
        isScheduled: !!options.scheduleDate || undefined
      });
    });

    const sentRequestOptions: InvokeApiOptions = {};
    if(this.pendingAfterMsgs[peerId]) {
      sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId;
    }

    const promise = /* true ? Promise.resolve() :  */apiManager.invokeApiAfter('messages.forwardMessages', {
      from_peer: appPeersManager.getInputPeerById(fromPeerId),
      id: mids.map(mid => this.getServerMessageId(mid)),
      random_id: newMessages.map(message => message.random_id),
      to_peer: appPeersManager.getInputPeerById(peerId),
      with_my_score: options.withMyScore,
      silent: options.silent,
      schedule_date: options.scheduleDate
    }, sentRequestOptions).then((updates) => {
      this.log('forwardMessages updates:', updates);
      apiUpdatesManager.processUpdateMessage(updates);
    }).finally(() => {
      if(this.pendingAfterMsgs[peerId] === sentRequestOptions) {
        delete this.pendingAfterMsgs[peerId];
      }
    });

    this.pendingAfterMsgs[peerId] = sentRequestOptions;
    return promise;
  }

  public getMessageFromStorage(storage: MessagesStorage, messageId: number) {
    return storage && storage[messageId] || {
      _: 'messageEmpty',
      id: messageId,
      deleted: true,
      pFlags: {}
    };
  }

  private createMessageStorage() {
    const storage: MessagesStorage = {} as any;
    
    /* let num = 0;
    Object.defineProperty(storage, 'num', {
      get: () => ++num,
      set: (_num: number) => num = _num, 
      enumerable: false
    });

    Object.defineProperty(storage, 'generateIndex', {
      value: (message: any) => {
        if(message.index === undefined) {
          message.index = (message.date * 0x10000) + (storage.num & 0xFFFF);
        }
      },
      enumerable: false
    }); */

    return storage;
  }

  public getMessagesStorage(peerId: number) {
    return this.messagesStorageByPeerId[peerId] ?? (this.messagesStorageByPeerId[peerId] = this.createMessageStorage());
  }

  public getMessageById(messageId: number) {
    for(const peerId in this.messagesStorageByPeerId) {
      if(appPeersManager.isChannel(+peerId)) {
        continue;
      }

      const message = this.messagesStorageByPeerId[peerId][messageId];
      if(message) {
        return message;
      }
    }

    return this.getMessageFromStorage(null, messageId);
  }

  public getMessageByPeer(peerId: number, messageId: number) {
    if(!peerId) {
      return this.getMessageById(messageId);
    }

    return this.getMessageFromStorage(this.getMessagesStorage(peerId), messageId);
  }

  public getMessagePeer(message: any): number {
    const toId = message.peer_id && appPeersManager.getPeerId(message.peer_id) || 0;

    return toId;
  }

  public getDialogByPeerId(peerId: number): [Dialog, number] | [] {
    return this.dialogsStorage.getDialog(peerId);
  }

  public reloadConversation(peerId: number | number[]) {
    [].concat(peerId).forEach(peerId => {
      if(!this.reloadConversationsPeers.includes(peerId)) {
        this.reloadConversationsPeers.push(peerId);
        //this.log('will reloadConversation', peerId);
      }
    });

    if(this.reloadConversationsPromise) return this.reloadConversationsPromise;
    return this.reloadConversationsPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        const peers = this.reloadConversationsPeers.map(peerId => appPeersManager.getInputDialogPeerById(peerId));
        this.reloadConversationsPeers.length = 0;

        apiManager.invokeApi('messages.getPeerDialogs', {peers}).then((result) => {
          this.applyConversations(result);
          resolve();
        }, reject).finally(() => {
          this.reloadConversationsPromise = null;
        });
      }, 0);
    });
  }

  private doFlushHistory(inputPeer: any, justClear?: boolean, revoke?: boolean): Promise<true> {
    return apiManager.invokeApi('messages.deleteHistory', {
      just_clear: justClear,
      revoke: revoke,
      peer: inputPeer,
      max_id: 0
    }).then((affectedHistory) => {
      apiUpdatesManager.processUpdateMessage({
        _: 'updateShort',
        update: {
          _: 'updatePts',
          pts: affectedHistory.pts,
          pts_count: affectedHistory.pts_count
        }
      });

      if(!affectedHistory.offset) {
        return true;
      }

      return this.doFlushHistory(inputPeer, justClear);
    })
  }

  public async flushHistory(peerId: number, justClear?: boolean, revoke?: boolean) {
    if(appPeersManager.isChannel(peerId)) {
      const promise = this.getHistory(peerId, 0, 1);

      const historyResult = promise instanceof Promise ? await promise : promise;

      const channelId = -peerId;
      const maxId = historyResult.history[0] || 0;
      return apiManager.invokeApi('channels.deleteHistory', {
        channel: appChatsManager.getChannelInput(channelId),
        max_id: maxId
      }).then(() => {
        apiUpdatesManager.processUpdateMessage({
          _: 'updateShort',
          update: {
            _: 'updateChannelAvailableMessages',
            channel_id: channelId,
            available_min_id: maxId
          }
        });

        return true;
      });
    }

    return this.doFlushHistory(appPeersManager.getInputPeerById(peerId), justClear, revoke).then(() => {
      delete this.historiesStorage[peerId];
      delete this.messagesStorageByPeerId[peerId];

      if(justClear) {
        rootScope.broadcast('dialog_flush', {peerId});
      } else {
        this.dialogsStorage.dropDialog(peerId);

        rootScope.broadcast('dialog_drop', {peerId});
      }
    });
  }

  public hidePinnedMessages(peerId: number) {
    return Promise.all([
      appStateManager.getState(),
      this.getPinnedMessage(peerId)
    ])
    .then(([state, pinned]) => {
      state.hiddenPinnedMessages[peerId] = pinned.maxId;
      rootScope.broadcast('peer_pinned_hidden', {peerId, maxId: pinned.maxId});
    });
  }

  public getPinnedMessage(peerId: number) {
    const p = this.pinnedMessages[peerId] ?? (this.pinnedMessages[peerId] = {});
    if(p.promise) return p.promise;
    else if(p.maxId) return Promise.resolve(p);

    return p.promise = this.getSearch({
      peerId, 
      inputFilter: {_: 'inputMessagesFilterPinned'},
      maxId: 0,
      limit: 1
    }).then(result => {
      p.count = result.count;
      p.maxId = result.history[0]?.mid;
      return p;
    }).finally(() => {
      delete p.promise;
    });
  }

  public updatePinnedMessage(peerId: number, mid: number, unpin?: true, silent?: true, oneSide?: true) {
    return apiManager.invokeApi('messages.updatePinnedMessage', {
      peer: appPeersManager.getInputPeerById(peerId),
      unpin,
      silent,
      pm_oneside: oneSide,
      id: this.getServerMessageId(mid)
    }).then(updates => {
      //this.log('pinned updates:', updates);
      apiUpdatesManager.processUpdateMessage(updates);
    });
  }

  public unpinAllMessages(peerId: number): Promise<boolean> {
    return apiManager.invokeApi('messages.unpinAllMessages', {
      peer: appPeersManager.getInputPeerById(peerId)
    }).then(affectedHistory => {
      apiUpdatesManager.processUpdateMessage({
        _: 'updateShort',
        update: {
          _: 'updatePts',
          pts: affectedHistory.pts,
          pts_count: affectedHistory.pts_count
        }
      });

      if(!affectedHistory.offset) {
        const storage = this.getMessagesStorage(peerId);
        for(const mid in storage) {
          const message = storage[mid];
          if(message.pFlags.pinned) {
            delete message.pFlags.pinned;
          }
        }

        rootScope.broadcast('peer_pinned_messages', {peerId, unpinAll: true});
        delete this.pinnedMessages[peerId];

        return true;
      }

      return this.unpinAllMessages(peerId);
    });
  }

  public getAlbumText(grouped_id: string) {
    const group = this.groupedMessagesStorage[grouped_id];
    let foundMessages = 0, message: string, totalEntities: MessageEntity[], entities: MessageEntity[];
    for(const i in group) {
      const m = group[i];
      if(m.message) {
        if(++foundMessages > 1) break;
        message = m.message;
        totalEntities = m.totalEntities;
        entities = m.entities;
      }  
    }

    if(foundMessages > 1) {
      message = undefined;
      totalEntities = undefined;
      entities = undefined;
    }

    return {message, entities, totalEntities};
  }

  public getMidsByAlbum(grouped_id: string) {
    return getObjectKeysAndSort(this.groupedMessagesStorage[grouped_id], 'asc');
    //return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b);
  }

  public getMidsByMessage(message: any) {
    if(message?.grouped_id) return this.getMidsByAlbum(message.grouped_id);
    else return [message.mid];
  }

  public filterMessages(message: any, verify: (message: MyMessage) => boolean) {
    const out: MyMessage[] = [];
    if(message.grouped_id) {
      const storage = this.groupedMessagesStorage[message.grouped_id];
      for(const mid in storage) {
        const message = storage[mid];
        if(verify(message)) {
          out.push(message);
        }
      }
    } else {
      if(verify(message)) {
        out.push(message);
      }
    }

    return out;
  }

  public generateTempMessageId(peerId: number) {
    const dialog = this.getDialogByPeerId(peerId)[0];
    return this.generateMessageId(dialog?.top_message || 0, true);
  }

  public generateMessageId(messageId: number, temp = false) {
    const q = AppMessagesManager.MESSAGE_ID_OFFSET;
    const num = temp ? ++this.tempNum : 0;
    if(messageId >= q) {
      if(temp) {
        return messageId + (num & (AppMessagesManager.MESSAGE_ID_INCREMENT - 1));
      }

      return messageId;
    }

    return q + (messageId * AppMessagesManager.MESSAGE_ID_INCREMENT + (num & (AppMessagesManager.MESSAGE_ID_INCREMENT - 1)));
  }

  /**
   * * will ignore outgoing offset
   */
  public getServerMessageId(messageId: number) {
    const q = AppMessagesManager.MESSAGE_ID_OFFSET;
    if(messageId < q) { // id 0 -> mid 0xFFFFFFFF, so 0xFFFFFFFF must convert to 0
      return messageId;
    }

    const l = AppMessagesManager.MESSAGE_ID_INCREMENT - 1;
    const used = messageId & l;
    if(used !== l) {
      messageId -= used + 1;
    }

    return (messageId - q) / AppMessagesManager.MESSAGE_ID_INCREMENT;
  }

  public incrementMessageId(messageId: number, increment: number) {
    return this.generateMessageId(this.getServerMessageId(messageId) + increment);
  }

  public saveMessages(messages: any[], options: Partial<{
    storage: MessagesStorage,
    isScheduled: true,
    isOutgoing: true
  }> = {}) {
    //let groups: Set<string>;
    messages.forEach((message) => {
      if(message.pFlags === undefined) {
        message.pFlags = {};
      }

      if(message._ === 'messageEmpty') {
        return;
      }

      // * exclude from state
      // defineNotNumerableProperties(message, ['rReply', 'mid', 'savedFrom', 'fwdFromId', 'fromId', 'peerId', 'reply_to_mid', 'viaBotId']);

      const peerId = this.getMessagePeer(message);
      const storage = options.storage || this.getMessagesStorage(peerId);
      const isChannel = message.peer_id._ === 'peerChannel';
      const channelId = isChannel ? -peerId : 0;
      const isBroadcast = isChannel && appChatsManager.isBroadcast(channelId);

      if(options.isScheduled) {
        message.pFlags.is_scheduled = true;
      }

      if(options.isOutgoing) {
        message.pFlags.is_outgoing = true;
      }
      
      const mid = this.generateMessageId(message.id);
      message.mid = mid;

      if(message.grouped_id) {
        const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = {});
        storage[mid] = message;
      }

      const dialog = this.getDialogByPeerId(peerId)[0];
      if(dialog && mid) {
        if(mid > dialog[message.pFlags.out
          ? 'read_outbox_max_id'
          : 'read_inbox_max_id']) {
          message.pFlags.unread = true;
        }
      }
      // this.log(dT(), 'msg unread', mid, apiMessage.pFlags.out, dialog && dialog[apiMessage.pFlags.out ? 'read_outbox_max_id' : 'read_inbox_max_id'])

      if(message.reply_to) {
        if(message.reply_to.reply_to_msg_id) {
          message.reply_to.reply_to_msg_id = message.reply_to_mid = this.generateMessageId(message.reply_to.reply_to_msg_id);
        } 

        if(message.reply_to.reply_to_top_id) message.reply_to.reply_to_top_id = this.generateMessageId(message.reply_to.reply_to_top_id);
      }

      if(message.replies) {
        if(message.replies.max_id) message.replies.max_id = this.generateMessageId(message.replies.max_id);
        if(message.replies.read_max_id) message.replies.read_max_id = this.generateMessageId(message.replies.read_max_id);
      }

      const overwriting = !!message.peerId;
      if(!overwriting) {
        message.date -= serverTimeManager.serverTimeOffset;
      }
      
      //storage.generateIndex(message);
      const myId = appUsersManager.getSelf().id;

      message.peerId = peerId;
      if(message.peerId === myId/*  && !message.from_id && !message.fwd_from */) {
        message.fromId = message.fwd_from ? (message.fwd_from.from_id ? appPeersManager.getPeerId(message.fwd_from.from_id) : 0) : myId;
      } else {
        //message.fromId = message.pFlags.post || (!message.pFlags.out && !message.from_id) ? peerId : appPeersManager.getPeerId(message.from_id);
        message.fromId = message.pFlags.post || !message.from_id ? peerId : appPeersManager.getPeerId(message.from_id);
      }

      const fwdHeader = message.fwd_from as MessageFwdHeader;
      if(fwdHeader) {
        //if(peerId === myID) {
          if(fwdHeader.saved_from_msg_id) fwdHeader.saved_from_msg_id = this.generateMessageId(fwdHeader.saved_from_msg_id);
          if(fwdHeader.channel_post) fwdHeader.channel_post = this.generateMessageId(fwdHeader.channel_post);

          const peer = fwdHeader.saved_from_peer || fwdHeader.from_id;
          const msgId = fwdHeader.saved_from_msg_id || fwdHeader.channel_post;
          if(peer && msgId) {
            const savedFromPeerId = appPeersManager.getPeerId(peer);
            const savedFromMid = this.generateMessageId(msgId);
            message.savedFrom = savedFromPeerId + '_' + savedFromMid;
          }

          /* if(peerId < 0 || peerId === myID) {
            message.fromId = appPeersManager.getPeerID(!message.from_id || deepEqual(message.from_id, fwdHeader.from_id) ? fwdHeader.from_id : message.from_id);
          } */
        /* } else {
          apiMessage.fwdPostID = fwdHeader.channel_post;
        } */

        message.fwdFromId = appPeersManager.getPeerId(fwdHeader.from_id);

        if(!overwriting) {
          fwdHeader.date -= serverTimeManager.serverTimeOffset;
        }
      }

      if(message.via_bot_id > 0) {
        message.viaBotId = message.via_bot_id;
      }

      const mediaContext: ReferenceContext = {
        type: 'message',
        peerId,
        messageId: mid
      };

      if(message.media) {
        switch(message.media._) {
          case 'messageMediaEmpty':
            delete message.media;
            break;
          case 'messageMediaPhoto':
            if(message.media.ttl_seconds) {
              message.media = {_: 'messageMediaUnsupportedWeb'};
            } else {
              message.media.photo = appPhotosManager.savePhoto(message.media.photo, mediaContext);
            }

            if(!message.media.photo) { // * found this bug on test DC
              delete message.media;
            }
            
            break;
          case 'messageMediaPoll':
            message.media.poll = appPollsManager.savePoll(message.media.poll, message.media.results);
            break;
          case 'messageMediaDocument':
            if(message.media.ttl_seconds) {
              message.media = {_: 'messageMediaUnsupportedWeb'};
            } else {
              message.media.document = appDocsManager.saveDoc(message.media.document, mediaContext); // 11.04.2020 warning
            }

            break;
          case 'messageMediaWebPage':
            message.media.webpage = appWebPagesManager.saveWebPage(message.media.webpage, message.mid, mediaContext);
            break;
          /*case 'messageMediaGame':
            AppGamesManager.saveGame(apiMessage.media.game, apiMessage.mid, mediaContext);
            apiMessage.media.handleMessage = true;
            break; */
          case 'messageMediaInvoice':
            message.media = {_: 'messageMediaUnsupportedWeb'};
            break;
        }
      }

      if(message.action) {
        let migrateFrom: number;
        let migrateTo: number;
        switch(message.action._) {
          //case 'messageActionChannelEditPhoto':
          case 'messageActionChatEditPhoto':
            message.action.photo = appPhotosManager.savePhoto(message.action.photo, mediaContext);
            if(message.action.photo.video_sizes) {
              message.action._ = isBroadcast ? 'messageActionChannelEditVideo' : 'messageActionChatEditVideo';
            } else {
              if(isBroadcast) { // ! messageActionChannelEditPhoto не существует в принципе, это используется для перевода.
                message.action._ = 'messageActionChannelEditPhoto';
              }
            }
            break;

          case 'messageActionChatEditTitle':
            if(isBroadcast) {
              message.action._ = 'messageActionChannelEditTitle';
            }
            break;

          case 'messageActionChatDeletePhoto':
            if(isBroadcast) {
              message.action._ = 'messageActionChannelDeletePhoto';
            }
            break;

          case 'messageActionChatAddUser':
            if(message.action.users.length === 1) {
              message.action.user_id = message.action.users[0];
              if(message.fromId === message.action.user_id) {
                let suffix = message.fromId === appUsersManager.getSelf().id ? 'You' : '';
                if(isChannel) {
                  message.action._ = 'messageActionChatJoined' + suffix;
                } else {
                  message.action._ = 'messageActionChatReturn' + suffix;
                }
              }
            } else if(message.action.users.length > 1) {
              message.action._ = 'messageActionChatAddUsers';
            }
            break;

          case 'messageActionChatDeleteUser':
            if(message.fromId === message.action.user_id) {
              message.action._ = 'messageActionChatLeave';
            }
            break;

          case 'messageActionChannelMigrateFrom':
            migrateFrom = -message.action.chat_id;
            migrateTo = -channelId;
            break

          case 'messageActionChatMigrateTo':
            migrateFrom = -channelId;
            migrateTo = -message.action.channel_id;
            break;

          case 'messageActionHistoryClear':
            //apiMessage.deleted = true;
            message.clear_history = true;
            delete message.pFlags.out;
            delete message.pFlags.unread;
            break;

          case 'messageActionPhoneCall':
            delete message.fromId;
            message.action.type = 
              (message.pFlags.out ? 'out_' : 'in_') +
              (
                message.action.reason._ === 'phoneCallDiscardReasonMissed' ||
                message.action.reason._ === 'phoneCallDiscardReasonBusy'
                   ? 'missed'
                   : 'ok'
              );
            break;
        }
        
        if(migrateFrom &&
            migrateTo &&
            !this.migratedFromTo[migrateFrom] &&
            !this.migratedToFrom[migrateTo]) {
          this.migrateChecks(migrateFrom, migrateTo);
        }
      }

      /* if(message.grouped_id) {
        if(!groups) {
          groups = new Set();
        }

        groups.add(message.grouped_id);
      } else {
        message.rReply = this.getRichReplyText(message);
      } */

      if(message.message && message.message.length && !message.totalEntities) {
        const myEntities = RichTextProcessor.parseEntities(message.message);
        const apiEntities = message.entities || [];
        message.totalEntities = RichTextProcessor.mergeEntities(apiEntities, myEntities); // ! only in this order, otherwise bold and emoji formatting won't work
      }

      storage[mid] = message;
    });

    /* if(groups) {
      for(const groupId of groups) {
        const mids = this.groupedMessagesStorage[groupId];
        for(const mid in mids) {
          const message = this.groupedMessagesStorage[groupId][mid];
          message.rReply = this.getRichReplyText(message);
        }
      }
    } */
  }

  public wrapMessageForReply(message: any, text: string, usingMids: number[], plain: true, highlightWord?: string): string;
  public wrapMessageForReply(message: any, text?: string, usingMids?: number[], plain?: false, highlightWord?: string): DocumentFragment;
  public wrapMessageForReply(message: any, text: string = message.message, usingMids?: number[], plain?: boolean, highlightWord?: string): DocumentFragment | string {
    const parts: (HTMLElement | string)[] = [];

    const addPart = (langKey: LangPackKey, part?: string | HTMLElement, text?: string) => {
      if(langKey) {
        part = plain ? I18n.format(langKey, true) : i18n(langKey);
      }
      
      if(plain) {
        parts.push(part);
      } else {
        const el = document.createElement('i');
        if(typeof(part) === 'string') el.innerHTML = part;
        else el.append(part);
        parts.push(el);
      }

      if(text) {
        parts.push(', ');
      }
    };

    if(message.media) {
      let usingFullAlbum = true;
      if(message.grouped_id) {
        if(usingMids) {
          const mids = this.getMidsByMessage(message);
          if(usingMids.length === mids.length) {
            for(const mid of mids) {
              if(!usingMids.includes(mid)) {
                usingFullAlbum = false;
                break;
              }
            }
          } else {
            usingFullAlbum = false;
          }
        }

        if(usingFullAlbum) {
          text = this.getAlbumText(message.grouped_id).message;
          addPart('AttachAlbum', undefined, text);
        }
      } else {
        usingFullAlbum = false;
      }

      if(!usingFullAlbum) {
        const media = message.media;
        switch(media._) {
          case 'messageMediaPhoto':
            addPart('AttachPhoto', undefined, message.message);
            break;
          case 'messageMediaDice':
            addPart(undefined, plain ? media.emoticon : RichTextProcessor.wrapEmojiText(media.emoticon));
            break;
          case 'messageMediaVenue': {
            const text = plain ? media.title : RichTextProcessor.wrapEmojiText(media.title);
            addPart('AttachLocation', undefined, text);
            parts.push(htmlToDocumentFragment(text) as any);
            break;
          }
          case 'messageMediaGeo':
            addPart('AttachLocation');
            break;
          case 'messageMediaGeoLive':
            addPart('AttachLiveLocation');
            break;
          case 'messageMediaPoll':
            addPart(undefined, plain ? '📊' + ' ' + (media.poll.question || 'poll') : media.poll.rReply);
            break;
          case 'messageMediaContact':
            addPart('AttachContact');
            break;
          case 'messageMediaGame': {
            const prefix = '🎮' + ' ';
            addPart(undefined, plain ? prefix + media.game.title : RichTextProcessor.wrapEmojiText(prefix + media.game.title));
            break;
          }
          case 'messageMediaDocument':
            let document = media.document;
  
            if(document.type === 'video') {
              addPart('AttachVideo', undefined, message.message);
            } else if(document.type === 'voice') {
              addPart('AttachAudio', undefined, message.message);
            } else if(document.type === 'gif') {
              addPart('AttachGif', undefined, message.message);
            } else if(document.type === 'round') {
              addPart('AttachRound', undefined, message.message);
            } else if(document.type === 'sticker') {
              addPart(undefined, ((plain ? document.stickerEmojiRaw : document.stickerEmoji) || '') + 'Sticker');
              text = '';
            } else {
              addPart(document.file_name, undefined, message.message);
            }
  
            break;
  
          default:
            //messageText += media._;
            ///////this.log.warn('Got unknown media type!', message);
            break;
        }
      } 
    }

    if(message.action) {
      const actionWrapped = this.wrapMessageActionTextNew(message, plain);
      if(actionWrapped) {
        addPart(undefined, actionWrapped);
      }
    }

    if(text) {
      text = limitSymbols(text, 100);

      if(plain) {
        parts.push(text);
      } else {
        let entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' '));

        if(highlightWord) {
          if(!entities) entities = [];
          let found = false;
          let match: any;
          let regExp = new RegExp(escapeRegExp(highlightWord), 'gi');
          while((match = regExp.exec(text)) !== null) {
            entities.push({_: 'messageEntityHighlight', length: highlightWord.length, offset: match.index});
            found = true;
          }
      
          if(found) {
            entities.sort((a, b) => a.offset - b.offset);
          }
        }

        const messageWrapped = RichTextProcessor.wrapRichText(text, {
          noLinebreaks: true, 
          entities, 
          noLinks: true,
          noTextFormat: true
        });
  
        parts.push(htmlToDocumentFragment(messageWrapped) as any);
      }
    }

    if(plain) {
      return parts.join('');
    } else {
      const fragment = document.createDocumentFragment();
      fragment.append(...parts);
      return fragment;
    }
  }

  public getSenderToPeerText(message: MyMessage) {
    let senderTitle = '', peerTitle: string;
    
    senderTitle = message.pFlags.out ? 'You' : appPeersManager.getPeerTitle(message.fromId, false, false);
    peerTitle = appPeersManager.isAnyGroup(message.peerId) || (message.pFlags.out && message.peerId !== rootScope.myId) ? 
      appPeersManager.getPeerTitle(message.peerId, false, false) : 
      '';

    if(peerTitle) {
      senderTitle += ' ➝ ' + peerTitle;
    }

    return senderTitle;
  }

  public wrapMessageActionTextNew(message: any, plain: true): string;
  public wrapMessageActionTextNew(message: any, plain?: false): HTMLElement;
  public wrapMessageActionTextNew(message: any, plain: boolean): HTMLElement | string;
  public wrapMessageActionTextNew(message: any, plain?: boolean): HTMLElement | string {
    const element: HTMLElement = plain ? undefined : document.createElement('span');
    const action = message.action as MessageAction;

    // this.log('message action:', action);

    if((action as MessageAction.messageActionCustomAction).message) {
      if(plain) {
        return RichTextProcessor.wrapPlainText(message.message);
      } else {
        element.innerHTML = RichTextProcessor.wrapRichText((action as MessageAction.messageActionCustomAction).message, {noLinebreaks: true});
        return element;
      }
    } else {
      let _ = action._;
      //let suffix = '';
      let langPackKey: LangPackKey;
      let args: any[];

      const getNameDivHTML = (peerId: number, plain: boolean) => {
        return plain ? appPeersManager.getPeerTitle(peerId, plain) + ' ' : (new PeerTitle({peerId})).element;
      };

      switch(action._) {
        case "messageActionPhoneCall": {
          _ += '.' + (action as any).type;

          const duration = action.duration || 1;
          const d: string[] = [];
    
          d.push(duration % 60 + ' s');
          if(duration >= 60) d.push((duration / 60 | 0) + ' min');
          //if(duration >= 3600) d.push((duration / 3600 | 0) + ' h');

          langPackKey = langPack[_];
          args = [d.reverse().join(' ')];
          break;
        }

        case 'messageActionPinMessage':
        case 'messageActionContactSignUp':
        case 'messageActionChatReturn':
        case 'messageActionChatLeave':
        case 'messageActionChatJoined':
        case 'messageActionChatCreate':
        case 'messageActionChatEditPhoto':
        case 'messageActionChatDeletePhoto':
        case 'messageActionChatEditVideo':
        case 'messageActionChatJoinedByLink':
        case 'messageActionChannelEditVideo':
        case 'messageActionChannelDeletePhoto': {
          langPackKey = langPack[_];
          args = [getNameDivHTML(message.fromId, plain)];
          break;
        }

        case 'messageActionChannelEditTitle':
        case 'messageActionChatEditTitle': {
          langPackKey = langPack[_];
          
          args = [];
          if(action._ === 'messageActionChatEditTitle') {
            args.push(getNameDivHTML(message.fromId, plain));
          }

          args.push(plain ? action.title : htmlToSpan(RichTextProcessor.wrapEmojiText(action.title)));
          break;
        }

        case 'messageActionChatDeleteUser':
        case 'messageActionChatAddUsers':
        case 'messageActionChatAddUser': {
          const users: number[] = (action as MessageAction.messageActionChatAddUser).users 
            || [(action as MessageAction.messageActionChatDeleteUser).user_id];

          langPackKey = langPack[_];
          args = [getNameDivHTML(message.fromId, plain)];

          if(users.length > 1) {
            if(plain) {
              args.push(...users.map((userId: number) => (getNameDivHTML(userId, true) as string).trim()).join(', '));
            } else {
              const fragment = document.createElement('span');
              fragment.append(
                ...join(
                  users.map((userId: number) => getNameDivHTML(userId, false)) as HTMLElement[],
                  false
                )
              );
              args.push(fragment);
            }
          } else {
            args.push(getNameDivHTML(users[0], plain));
          }

          break;
        }

        case 'messageActionBotAllowed': {
          const anchorHTML = RichTextProcessor.wrapRichText(action.domain, {
            entities: [{
              _: 'messageEntityUrl',
              length: action.domain.length,
              offset: 0
            }]
          });

          const node = htmlToSpan(anchorHTML);
          
          langPackKey = langPack[_];
          args = [node];
          break;
        }

        default:
          langPackKey = (langPack[_] || `[${action._}]`) as any;
          break;
      }

      if(!langPackKey) {
        langPackKey = langPack[_];
        if(langPackKey === undefined) {
          langPackKey = '[' + _ + ']' as any;
        }
      }

      if(plain) {
        return I18n.format(langPackKey, true, args);
      } else {
        return _i18n(element, langPackKey, args);
      }

      //str = !langPackKey || langPackKey[0].toUpperCase() === langPackKey[0] ? langPackKey : getNameDivHTML(message.fromId) + langPackKey + (suffix ? ' ' : '');
    }
  }

  public editPeerFolders(peerIds: number[], folderId: number) {
    apiManager.invokeApi('folders.editPeerFolders', {
      folder_peers: peerIds.map(peerId => {
        return {
          _: 'inputFolderPeer',
          peer: appPeersManager.getInputPeerById(peerId),
          folder_id: folderId
        };
      })
    }).then(updates => {
      //this.log('editPeerFolders updates:', updates);
      apiUpdatesManager.processUpdateMessage(updates); // WARNING! возможно тут нужно добавлять channelId, и вызывать апдейт для каждого канала отдельно
    });
  }

  public toggleDialogPin(peerId: number, filterId?: number) {
    if(filterId > 1) {
      this.filtersStorage.toggleDialogPin(peerId, filterId);
      return;
    }

    const dialog = this.getDialogByPeerId(peerId)[0];
    if(!dialog) return Promise.reject();

    const pinned = dialog.pFlags?.pinned ? undefined : true;
    return apiManager.invokeApi('messages.toggleDialogPin', {
      peer: appPeersManager.getInputDialogPeerById(peerId),
      pinned
    }).then(bool => {
      if(bool) {
        const pFlags: Update.updateDialogPinned['pFlags'] = pinned ? {pinned} : {};
        this.handleUpdate({
          _: 'updateDialogPinned',
          peer: appPeersManager.getDialogPeer(peerId),
          folder_id: filterId,
          pFlags
        });
      }
    });
  }

  public markDialogUnread(peerId: number, read?: true) {
    const dialog = this.getDialogByPeerId(peerId)[0];
    if(!dialog) return Promise.reject();

    const unread = read || dialog.pFlags?.unread_mark ? undefined : true;
    return apiManager.invokeApi('messages.markDialogUnread', {
      peer: appPeersManager.getInputDialogPeerById(peerId),
      unread
    }).then(bool => {
      if(bool) {
        const pFlags: Update.updateDialogUnreadMark['pFlags'] = unread ? {unread} : {};
        this.handleUpdate({
          _: 'updateDialogUnreadMark',
          peer: appPeersManager.getDialogPeer(peerId),
          pFlags
        });
      }
    });
  }

  public migrateChecks(migrateFrom: number, migrateTo: number) {
    if(!this.migratedFromTo[migrateFrom] &&
      !this.migratedToFrom[migrateTo] &&
      appChatsManager.hasChat(-migrateTo)) {
      const fromChat = appChatsManager.getChat(-migrateFrom);
      if(fromChat &&
        fromChat.migrated_to &&
        fromChat.migrated_to.channel_id === -migrateTo) {
          this.migratedFromTo[migrateFrom] = migrateTo;
          this.migratedToFrom[migrateTo] = migrateFrom;

        //setTimeout(() => {
          rootScope.broadcast('dialog_migrate', {migrateFrom, migrateTo});

          const dropped = this.dialogsStorage.dropDialog(migrateFrom);
          if(dropped.length) {
            rootScope.broadcast('dialog_drop', {peerId: migrateFrom, dialog: dropped[0]});
          }
        //}, 100);
      }
    }
  }

  public canMessageBeEdited(message: any, kind: 'text' | 'poll') {
    const goodMedias = [
      'messageMediaPhoto',
      'messageMediaDocument',
      'messageMediaWebPage'
    ];

    if(kind === 'poll') {
      goodMedias.push('messageMediaPoll');
    }

    if(message._ !== 'message' ||
        message.deleted ||
        message.fwd_from ||
        message.via_bot_id ||
        message.media && goodMedias.indexOf(message.media._) === -1 ||
        message.fromId && appUsersManager.isBot(message.fromId)) {
      return false;
    }
    
    if(message.media &&
        message.media._ === 'messageMediaDocument' &&
        (message.media.document.sticker || message.media.document.type === 'round')) {
      return false;
    }

    return true;
  }

  public canEditMessage(message: any, kind: 'text' | 'poll' = 'text') {
    if(!message || !this.canMessageBeEdited(message, kind)) {
      return false;
    }

    // * second rule for saved messages, because there is no 'out' flag
    if(message.pFlags.out || this.getMessagePeer(message) === appUsersManager.getSelf().id) {
      return true;
    }

    if((message.date < tsNow(true) - (2 * 86400) && message.media?._ !== 'messageMediaPoll') || !message.pFlags.out) {
      return false;
    }

    return true;
  }

  public canDeleteMessage(message: any) {
    return message && (
      message.peerId > 0 
      || message.fromId === rootScope.myId 
      || appChatsManager.getChat(message.peerId)._ === 'chat' 
      || appChatsManager.hasRights(message.peerId, 'delete_messages')
    );
  }

  public applyConversations(dialogsResult: MessagesPeerDialogs.messagesPeerDialogs) {
    // * В эту функцию попадут только те диалоги, в которых есть read_inbox_max_id и read_outbox_max_id, в отличие от тех, что будут в getTopMessages

    // ! fix 'dialogFolder', maybe there is better way to do it, this only can happen by 'messages.getPinnedDialogs' by folder_id: 0
    forEachReverse(dialogsResult.dialogs, (dialog, idx) => {
      if(dialog._ === 'dialogFolder') {
        dialogsResult.dialogs.splice(idx, 1);
      }
    });

    appUsersManager.saveApiUsers(dialogsResult.users);
    appChatsManager.saveApiChats(dialogsResult.chats);
    this.saveMessages(dialogsResult.messages);

    this.log('applyConversation', dialogsResult);

    const updatedDialogs: {[peerId: number]: Dialog} = {};
    (dialogsResult.dialogs as Dialog[]).forEach((dialog) => {
      const peerId = appPeersManager.getPeerId(dialog.peer);
      let topMessage = dialog.top_message;

      const topPendingMessage = this.pendingTopMsgs[peerId];
      if(topPendingMessage) {
        if(!topMessage 
          || (this.getMessageByPeer(peerId, topPendingMessage) as MyMessage).date > (this.getMessageByPeer(peerId, topMessage) as MyMessage).date) {
          dialog.top_message = topMessage = topPendingMessage;
          this.getHistoryStorage(peerId).maxId = topPendingMessage;
        }
      }

      /* const d = Object.assign({}, dialog);
      if(peerId === 239602833) {
        this.log.error('applyConversation lun', dialog, d);
      } */

      if(topMessage || (dialog.draft && dialog.draft._ === 'draftMessage')) {
        this.saveConversation(dialog);
        updatedDialogs[peerId] = dialog;
      } else {
        const dropped = this.dialogsStorage.dropDialog(peerId);
        if(dropped.length) {
          rootScope.broadcast('dialog_drop', {peerId, dialog: dropped[0]});
        }
      }

      if(this.newUpdatesAfterReloadToHandle[peerId] !== undefined) {
        for(const update of this.newUpdatesAfterReloadToHandle[peerId]) {
          this.handleUpdate(update);
        }

        delete this.newUpdatesAfterReloadToHandle[peerId];
      }
    });

    if(Object.keys(updatedDialogs).length) {
      rootScope.broadcast('dialogs_multiupdate', updatedDialogs);
    }
  }

  public generateDialog(peerId: number) {
    const dialog: Dialog = {
      _: 'dialog',
      pFlags: {},
      peer: appPeersManager.getOutputPeer(peerId),
      top_message: 0,
      read_inbox_max_id: 0,
      read_outbox_max_id: 0,
      unread_count: 0,
      unread_mentions_count: 0,
      notify_settings: {
        _: 'peerNotifySettings',
      },
    };

    return dialog;
  }

  /**
   * Won't save migrated from peer, forbidden peers, left and kicked
   */
  public saveConversation(dialog: Dialog, folderId = 0) {
    const peerId = appPeersManager.getPeerId(dialog.peer);
    if(!peerId) {
      console.error('saveConversation no peerId???', dialog, folderId);
      return false;
    }

    if(dialog._ !== 'dialog'/*  || peerId === 239602833 */) {
      console.error('saveConversation not regular dialog', dialog, Object.assign({}, dialog));
    }
    
    const channelId = appPeersManager.isChannel(peerId) ? -peerId : 0;

    if(peerId < 0) {
      const chat: Chat = appChatsManager.getChat(-peerId);
      if(chat._ === 'channelForbidden' || chat._ === 'chatForbidden' || (chat as Chat.chat).pFlags.left || (chat as Chat.chat).pFlags.kicked) {
        return false;
      }
    }

    const peerText = appPeersManager.getPeerSearchText(peerId);
    searchIndexManager.indexObject(peerId, peerText, this.dialogsIndex);

    let mid: number, message;
    if(dialog.top_message) {
      mid = this.generateMessageId(dialog.top_message);//dialog.top_message;
      message = this.getMessageByPeer(peerId, mid);
    } else {
      mid = this.generateTempMessageId(peerId);
      message = {
        _: 'message',
        id: mid,
        mid,
        from_id: appPeersManager.getOutputPeer(appUsersManager.getSelf().id),
        peer_id: appPeersManager.getOutputPeer(peerId),
        deleted: true,
        pFlags: {out: true},
        date: 0,
        message: ''
      };
      this.saveMessages([message], {isOutgoing: true});
    }

    if(!message?.pFlags) {
      this.log.error('saveConversation no message:', dialog, message);
    }

    if(!channelId && peerId < 0) {
      const chat = appChatsManager.getChat(-peerId);
      if(chat && chat.migrated_to && chat.pFlags.deactivated) {
        const migratedToPeer = appPeersManager.getPeerId(chat.migrated_to);
        this.migratedFromTo[peerId] = migratedToPeer;
        this.migratedToFrom[migratedToPeer] = peerId;
        return;
      }
    }

    const wasDialogBefore = this.getDialogByPeerId(peerId)[0];

    dialog.top_message = mid;
    dialog.read_inbox_max_id = this.generateMessageId(wasDialogBefore && !dialog.read_inbox_max_id ? wasDialogBefore.read_inbox_max_id : dialog.read_inbox_max_id);
    dialog.read_outbox_max_id = this.generateMessageId(wasDialogBefore && !dialog.read_outbox_max_id ? wasDialogBefore.read_outbox_max_id : dialog.read_outbox_max_id);

    if(!dialog.hasOwnProperty('folder_id')) {
      if(dialog._ === 'dialog') {
        // ! СЛОЖНО ! СМОТРИ В getTopMessages
        dialog.folder_id = wasDialogBefore ? wasDialogBefore.folder_id : folderId;
      }/*  else if(dialog._ === 'dialogFolder') {
        dialog.folder_id = dialog.folder.id;
      } */
    }

    dialog.draft = appDraftsManager.saveDraft(peerId, 0, dialog.draft);
    dialog.peerId = peerId;

    // Because we saved message without dialog present
    if(message.pFlags.is_outgoing) {
      if(mid > dialog[message.pFlags.out ? 'read_outbox_max_id' : 'read_inbox_max_id']) message.pFlags.unread = true;
      else delete message.pFlags.unread;
    }

    let historyStorage = this.getHistoryStorage(peerId);
    /* if(historyStorage === undefined) { // warning
      historyStorage.history.push(mid);
      if(this.mergeReplyKeyboard(historyStorage, message)) {
        rootScope.broadcast('history_reply_markup', {peerId});
      }
    } else  */if(!historyStorage.history.slice.length) {
      historyStorage.history.unshift(mid);
    }

    historyStorage.maxId = mid;
    historyStorage.readMaxId = dialog.read_inbox_max_id;
    historyStorage.readOutboxMaxId = dialog.read_outbox_max_id;

    appNotificationsManager.savePeerSettings(peerId, dialog.notify_settings)

    if(channelId && dialog.pts) {
      apiUpdatesManager.addChannelState(channelId, dialog.pts);
    }

    this.dialogsStorage.generateIndexForDialog(dialog);
    this.dialogsStorage.pushDialog(dialog, message.date);
  }

  public mergeReplyKeyboard(historyStorage: HistoryStorage, message: any) {
    // this.log('merge', message.mid, message.reply_markup, historyStorage.reply_markup)
    if(!message.reply_markup &&
      !message.pFlags?.out &&
      !message.action) {
      return false;
    }
    if(message.reply_markup &&
      message.reply_markup._ === 'replyInlineMarkup') {
      return false;
    }
    var messageReplyMarkup = message.reply_markup;
    var lastReplyMarkup = historyStorage.reply_markup;
    if(messageReplyMarkup) {
      if(lastReplyMarkup && lastReplyMarkup.mid >= message.mid) {
        return false;
      }

      if(messageReplyMarkup.pFlags.selective) {
        return false;
      }

      if(historyStorage.maxOutId &&
        message.mid < historyStorage.maxOutId &&
        messageReplyMarkup.pFlags.single_use) {
        messageReplyMarkup.pFlags.hidden = true;
      }
      messageReplyMarkup = Object.assign({
        mid: message.mid
      }, messageReplyMarkup);
      if(messageReplyMarkup._ !== 'replyKeyboardHide') {
        messageReplyMarkup.fromId = appPeersManager.getPeerId(message.from_id);
      }
      historyStorage.reply_markup = messageReplyMarkup;
      // this.log('set', historyStorage.reply_markup)
      return true;
    }

    if(message.pFlags.out) {
      if(lastReplyMarkup) {
        if(lastReplyMarkup.pFlags.single_use &&
          !lastReplyMarkup.pFlags.hidden &&
          (message.mid > lastReplyMarkup.mid || message.pFlags.is_outgoing) &&
          message.message) {
          lastReplyMarkup.pFlags.hidden = true;
          // this.log('set', historyStorage.reply_markup)
          return true;
        }
      } else if(!historyStorage.maxOutId ||
        message.mid > historyStorage.maxOutId) {
        historyStorage.maxOutId = message.mid;
      }
    }

    if(message.action &&
      message.action._ === 'messageActionChatDeleteUser' &&
      (lastReplyMarkup
        ? message.action.user_id === lastReplyMarkup.fromId
        : appUsersManager.isBot(message.action.user_id)
      )
    ) {
      historyStorage.reply_markup = {
        _: 'replyKeyboardHide',
        mid: message.mid,
        pFlags: {}
      };
      // this.log('set', historyStorage.reply_markup)
      return true;
    }

    return false;
  }

  public getSearchStorage(peerId: number, inputFilter: MyInputMessagesFilter) {
    if(!this.searchesStorage[peerId]) this.searchesStorage[peerId] = {};
    if(!this.searchesStorage[peerId][inputFilter]) this.searchesStorage[peerId][inputFilter] = {history: []};
    return this.searchesStorage[peerId][inputFilter];
  }

  public getSearchCounters(peerId: number, filters: MessagesFilter[]) {
    return apiManager.invokeApi('messages.getSearchCounters', {
      peer: appPeersManager.getInputPeerById(peerId),
      filters
    });
  }

  public getSearch({peerId, query, inputFilter, maxId, limit, nextRate, backLimit, threadId, folderId, minDate, maxDate}: {
    peerId?: number,
    maxId?: number,
    limit?: number,
    nextRate?: number,
    backLimit?: number,
    threadId?: number,
    folderId?: number,
    query?: string,
    inputFilter?: {
      _: MyInputMessagesFilter
    },
    minDate?: number,
    maxDate?: number
  }): Promise<{
    count: number,
    next_rate: number,
    offset_id_offset: number,
    history: MyMessage[]
  }> {
    if(!peerId) peerId = 0;
    if(!query) query = '';
    if(!inputFilter) inputFilter = {_: 'inputMessagesFilterEmpty'};
    if(limit === undefined) limit = 20;
    if(!nextRate) nextRate = 0;
    if(!backLimit) backLimit = 0;

    minDate = minDate ? minDate / 1000 | 0 : 0;
    maxDate = maxDate ? maxDate / 1000 | 0 : 0;

    const foundMsgs: Message.message[] = [];

    //this.log('search', maxId);

    if(backLimit) {
      limit += backLimit;
    }

    //const beta = inputFilter._ === 'inputMessagesFilterPinned' && !backLimit;
    const beta = false;

    let storage: {
      count?: number;
      history: SlicedArray;
    };

    // * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений
    if(peerId && !backLimit && !maxId && !query && limit !== 1 && !threadId/*  && inputFilter._ !== 'inputMessagesFilterPinned' */) {
      storage = beta ? 
        this.getSearchStorage(peerId, inputFilter._) as any : 
        this.getHistoryStorage(peerId);
      let filtering = true;

      const history = /* maxId ? storage.history.slice(storage.history.indexOf(maxId) + 1) :  */storage.history;

      if(storage !== undefined && history.length) {
        const neededContents: {
          [messageMediaType: string]: boolean
        } = {},
          neededDocTypes: string[] = [], 
          excludeDocTypes: string[] = []/* ,
          neededFlags: string[] = [] */;

        switch(inputFilter._) {
          case 'inputMessagesFilterPhotos':
            neededContents['messageMediaPhoto'] = true;
            break;

          case 'inputMessagesFilterPhotoVideo':
            neededContents['messageMediaPhoto'] = true;
            neededContents['messageMediaDocument'] = true;
            neededDocTypes.push('video');
            break;

          case 'inputMessagesFilterVideo':
            neededContents['messageMediaDocument'] = true;
            neededDocTypes.push('video');
            break;

          case 'inputMessagesFilterDocument':
            neededContents['messageMediaDocument'] = true;
            excludeDocTypes.push('video');
            break;

          case 'inputMessagesFilterVoice':
            neededContents['messageMediaDocument'] = true;
            neededDocTypes.push('voice');
            break;

          case 'inputMessagesFilterRoundVoice':
            neededContents['messageMediaDocument'] = true;
            neededDocTypes.push('round', 'voice');
            break;

          case 'inputMessagesFilterRoundVideo':
            neededContents['messageMediaDocument'] = true;
            neededDocTypes.push('round');
            break;

          case 'inputMessagesFilterMusic':
            neededContents['messageMediaDocument'] = true;
            neededDocTypes.push('audio');
            break;

          case 'inputMessagesFilterUrl':
            neededContents['url'] = true;
            break;

          case 'inputMessagesFilterChatPhotos':
            neededContents['avatar'] = true;
            break;

          /* case 'inputMessagesFilterPinned':
            neededFlags.push('pinned');
            break; */

          /* case 'inputMessagesFilterMyMentions':
            neededContents['mentioned'] = true;
            break; */

          default:
            filtering = false;
            break;
            /* return Promise.resolve({
              count: 0,
              next_rate: 0,
              history: [] as number[]
            }); */
        }

        if(filtering) {
          const storage = this.getMessagesStorage(peerId);
          for(let i = 0, length = history.length; i < length; i++) {
            const message = storage[history.slice[i]];

            if(!message) continue;
  
            //|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ === 'messageEntityMention'));
  
            let found = false;
            if(message.media && neededContents[message.media._] && !message.fwd_from) {
              if(message.media._ === 'messageMediaDocument') {
                if((neededDocTypes.length && !neededDocTypes.includes(message.media.document.type)) 
                  || excludeDocTypes.includes(message.media.document.type)) {
                  continue;
                }
              }
  
              found = true;
            } else if(neededContents['url'] && message.message) {
              const goodEntities = ['messageEntityTextUrl', 'messageEntityUrl'];
              if((message.totalEntities as MessageEntity[]).find(e => goodEntities.includes(e._)) || RichTextProcessor.matchUrl(message.message)) {
                found = true;
              }
            } else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto'].includes(message.action._)) {
              found = true;
            }/*  else if(neededFlags.find(flag => message.pFlags[flag])) {
              found = true;
            } */
  
            if(found) {
              foundMsgs.push(message);
              if(foundMsgs.length >= limit) {
                break;
              }
            }
          }
        }
      }
    }

    if(foundMsgs.length) {
      if(foundMsgs.length < limit && (beta ? storage.count !== storage.history.length : true)) {
        maxId = foundMsgs[foundMsgs.length - 1].mid;
        limit = limit - foundMsgs.length;
      } else {
        return Promise.resolve({
          count: beta ? storage.count : 0,
          next_rate: 0,
          offset_id_offset: 0,
          history: foundMsgs
        });
      }
    } else if(beta && storage?.count) {
      return Promise.resolve({
        count: storage.count,
        next_rate: 0,
        offset_id_offset: 0,
        history: []
      });
    }

    let apiPromise: Promise<any>;
    if(peerId && !nextRate && folderId === undefined/*  || !query */) {
      apiPromise = apiManager.invokeApi('messages.search', {
        peer: appPeersManager.getInputPeerById(peerId),
        q: query || '',
        filter: inputFilter as any as MessagesFilter,
        min_date: minDate,
        max_date: maxDate,
        limit,
        offset_id: this.getServerMessageId(maxId) || 0,
        add_offset: backLimit ? -backLimit : 0,
        max_id: 0,
        min_id: 0,
        hash: 0,
        top_msg_id: this.getServerMessageId(threadId) || 0
      }, {
        //timeout: APITIMEOUT,
        noErrorBox: true
      });
    } else {
      //var offsetDate = 0;
      let offsetPeerId = 0;
      let offsetId = 0;
      let offsetMessage = maxId && this.getMessageByPeer(peerId, maxId);

      if(offsetMessage && offsetMessage.date) {
        //offsetDate = offsetMessage.date + serverTimeManager.serverTimeOffset;
        offsetId = offsetMessage.id;
        offsetPeerId = this.getMessagePeer(offsetMessage);
      }

      apiPromise = apiManager.invokeApi('messages.searchGlobal', {
        q: query,
        filter: inputFilter as any as MessagesFilter,
        min_date: minDate,
        max_date: maxDate,
        offset_rate: nextRate,
        offset_peer: appPeersManager.getInputPeerById(offsetPeerId),
        offset_id: offsetId,
        limit,
        folder_id: folderId
      }, {
        //timeout: APITIMEOUT,
        noErrorBox: true
      });
    }

    return apiPromise.then((searchResult: any) => {
      appUsersManager.saveApiUsers(searchResult.users);
      appChatsManager.saveApiChats(searchResult.chats);
      this.saveMessages(searchResult.messages);

      /* if(beta && storage && (!maxId || storage.history[storage.history.length - 1] === maxId)) {
        const storage = this.getSearchStorage(peerId, inputFilter._);
        const add = (searchResult.messages.map((m: any) => m.mid) as number[]).filter(mid => storage.history.indexOf(mid) === -1);
        storage.history.push(...add);
        storage.history.sort((a, b) => b - a);
        storage.count = searchResult.count;
      } */

      if(DEBUG) {
        this.log('getSearch result:', inputFilter, searchResult);
      }

      const foundCount: number = searchResult.count || (foundMsgs.length + searchResult.messages.length);

      searchResult.messages.forEach((message: any) => {
        const peerId = this.getMessagePeer(message);
        if(peerId < 0) {
          const chat = appChatsManager.getChat(-peerId);
          if(chat.migrated_to) {
            this.migrateChecks(peerId, -chat.migrated_to.channel_id);
          }
        }

        foundMsgs.push(message);
      });

      return {
        count: foundCount,
        offset_id_offset: searchResult.offset_id_offset || 0,
        next_rate: searchResult.next_rate,
        history: foundMsgs
      };
    });
  }

  public subscribeRepliesThread(peerId: number, mid: number) {
    const repliesKey = peerId + '_' + mid;
    for(const threadKey in this.threadsToReplies) {
      if(this.threadsToReplies[threadKey] === repliesKey) return;
    }

    this.getDiscussionMessage(peerId, mid);
  }

  public generateThreadServiceStartMessage(message: Message.message) {
    const threadKey = message.peerId + '_' + message.mid;
    if(this.threadsServiceMessagesIdsStorage[threadKey]) return;

    const maxMessageId = this.getServerMessageId(Math.max(...this.getMidsByMessage(message)));
    const serviceStartMessage: Message.messageService = {
      _: 'messageService',
      pFlags: {
        is_single: true
      } as any,
      id: this.generateMessageId(maxMessageId, true),
      date: message.date,
      from_id: {_: 'peerUser', user_id: 0}/* message.from_id */,
      peer_id: message.peer_id,
      action: {
        _: 'messageActionCustomAction',
        message: 'Discussion started'
      },
      reply_to: this.generateReplyHeader(message.id)
    };

    this.saveMessages([serviceStartMessage], {isOutgoing: true});
    this.threadsServiceMessagesIdsStorage[threadKey] = serviceStartMessage.mid;
  } 

  public getDiscussionMessage(peerId: number, mid: number) {
    return apiManager.invokeApi('messages.getDiscussionMessage', {
      peer: appPeersManager.getInputPeerById(peerId),
      msg_id: this.getServerMessageId(mid)
    }).then(result => {
      appChatsManager.saveApiChats(result.chats);
      appUsersManager.saveApiUsers(result.users);
      this.saveMessages(result.messages);

      const message = this.filterMessages(result.messages[0], message => !!(message as Message.message).replies)[0] as Message.message;
      const threadKey = message.peerId + '_' + message.mid;

      this.generateThreadServiceStartMessage(message);
      
      const historyStorage = this.getHistoryStorage(message.peerId, message.mid);
      result.max_id = historyStorage.maxId = this.generateMessageId(result.max_id) || 0;
      result.read_inbox_max_id = historyStorage.readMaxId = this.generateMessageId(result.read_inbox_max_id) || 0;
      result.read_outbox_max_id = historyStorage.readOutboxMaxId = this.generateMessageId(result.read_outbox_max_id) || 0;

      this.threadsToReplies[threadKey] = peerId + '_' + mid;

      return message;
    });
  }

  handleNewMessages = () => {
    clearTimeout(this.newMessagesHandlePromise);
    this.newMessagesHandlePromise = 0;

    rootScope.broadcast('history_multiappend', this.newMessagesToHandle);
    this.newMessagesToHandle = {};
  };

  handleNewDialogs = () => {
    clearTimeout(this.newDialogsHandlePromise);
    this.newDialogsHandlePromise = 0;
    
    let newMaxSeenId = 0;
    for(const peerId in this.newDialogsToHandle) {
      const dialog = this.newDialogsToHandle[peerId];
      if('reload' in dialog) {
        this.reloadConversation(+peerId);
        delete this.newDialogsToHandle[peerId];
      } else {
        this.dialogsStorage.pushDialog(dialog);
        if(!appPeersManager.isChannel(+peerId)) {
          newMaxSeenId = Math.max(newMaxSeenId, dialog.top_message || 0);
        }
      }
    }

    //this.log('after order:', this.dialogsStorage[0].map(d => d.peerId));

    if(newMaxSeenId !== 0) {
      this.incrementMaxSeenId(newMaxSeenId);
    }

    rootScope.broadcast('dialogs_multiupdate', this.newDialogsToHandle as any);
    this.newDialogsToHandle = {};
  };

  public scheduleHandleNewDialogs() {
    if(!this.newDialogsHandlePromise) {
      this.newDialogsHandlePromise = window.setTimeout(this.handleNewDialogs, 0);
    }
  }

  public deleteMessages(peerId: number, mids: number[], revoke?: true) {
    let promise: Promise<any>;

    const localMessageIds = mids.map(mid => this.getServerMessageId(mid));

    if(peerId < 0 && appPeersManager.isChannel(peerId)) {
      const channelId = -peerId;
      const channel = appChatsManager.getChat(channelId);
      if(!channel.pFlags.creator && !(channel.pFlags.editor && channel.pFlags.megagroup)) {
        const goodMsgIds: number[] = [];
        if(channel.pFlags.editor || channel.pFlags.megagroup) {
          mids.forEach((msgId, i) => {
            const message = this.getMessageByPeer(peerId, mids[i]);
            if(message.pFlags.out) {
              goodMsgIds.push(msgId);
            }
          });
        }

        if(!goodMsgIds.length) {
          return;
        }

        mids = goodMsgIds;
      }

      promise = apiManager.invokeApi('channels.deleteMessages', {
        channel: appChatsManager.getChannelInput(channelId),
        id: localMessageIds
      }).then((affectedMessages) => {
        apiUpdatesManager.processUpdateMessage({
          _: 'updateShort',
          update: {
            _: 'updateDeleteChannelMessages',
            channel_id: channelId,
            messages: mids,
            pts: affectedMessages.pts,
            pts_count: affectedMessages.pts_count
          }
        });
      });
    } else {
      promise = apiManager.invokeApi('messages.deleteMessages', {
        revoke,
        id: localMessageIds
      }).then((affectedMessages) => {
        apiUpdatesManager.processUpdateMessage({
          _: 'updateShort',
          update: {
            _: 'updateDeleteMessages',
            messages: mids,
            pts: affectedMessages.pts,
            pts_count: affectedMessages.pts_count
          }
        });
      });
    }

    return promise;
  }

  public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) {
    // return Promise.resolve();
    // console.trace('start read')
    this.log('readHistory:', peerId, maxId, threadId);
    if(!this.getReadMaxIdIfUnread(peerId, threadId) && !force) {
      this.log('readHistory: isn\'t unread');
      return Promise.resolve();
    }

    const isChannel = appPeersManager.isChannel(peerId);
    const historyStorage = this.getHistoryStorage(peerId, threadId);

    let apiPromise: Promise<any>;
    if(threadId) {
      if(!historyStorage.readPromise) {
        apiPromise = apiManager.invokeApi('messages.readDiscussion', {
          peer: appPeersManager.getInputPeerById(peerId),
          msg_id: this.getServerMessageId(threadId),
          read_max_id: this.getServerMessageId(maxId)
        });
      }

      apiUpdatesManager.processUpdateMessage({
        _: 'updateShort',
        update: {
          _: 'updateReadChannelDiscussionInbox',
          channel_id: -peerId,
          top_msg_id: threadId,
          read_max_id: maxId
        } as Update.updateReadChannelDiscussionInbox
      });
    } else if(isChannel) {
      if(!historyStorage.readPromise) {
        apiPromise = apiManager.invokeApi('channels.readHistory', {
          channel: appChatsManager.getChannelInput(-peerId),
          max_id: this.getServerMessageId(maxId)
        });
      }

      apiUpdatesManager.processUpdateMessage({
        _: 'updateShort',
        update: {
          _: 'updateReadChannelInbox',
          max_id: maxId,
          channel_id: -peerId
        }
      });
    } else {
      if(!historyStorage.readPromise) {
        apiPromise = apiManager.invokeApi('messages.readHistory', {
          peer: appPeersManager.getInputPeerById(peerId),
          max_id: this.getServerMessageId(maxId)
        }).then((affectedMessages) => {
          apiUpdatesManager.processUpdateMessage({
            _: 'updateShort',
            update: {
              _: 'updatePts',
              pts: affectedMessages.pts,
              pts_count: affectedMessages.pts_count
            }
          });
        });
      }

      apiUpdatesManager.processUpdateMessage({
        _: 'updateShort',
        update: {
          _: 'updateReadHistoryInbox',
          max_id: maxId,
          peer: appPeersManager.getOutputPeer(peerId)
        }
      });
    }

    if(!threadId && historyStorage && historyStorage.history.length) {
      const slice = historyStorage.history.slice;
      for(const mid of slice) {
        const message = this.getMessageByPeer(peerId, mid);
        if(message && !message.pFlags.out) {
          message.pFlags.unread = false;
          appNotificationsManager.cancel('msg' + mid);
        }
      }
    }

    appNotificationsManager.soundReset(appPeersManager.getPeerString(peerId));

    if(historyStorage.readPromise) {
      return historyStorage.readPromise;
    }

    apiPromise.finally(() => {
      delete historyStorage.readPromise;

      this.log('readHistory: promise finally', maxId, historyStorage.readMaxId);

      if(historyStorage.readMaxId > maxId) {
        this.readHistory(peerId, historyStorage.readMaxId, threadId, true);
      }
    });

    return historyStorage.readPromise = apiPromise;
  }

  public readAllHistory(peerId: number, threadId?: number, force = false) {
    const historyStorage = this.getHistoryStorage(peerId, threadId);
    if(historyStorage.maxId) {
      this.readHistory(peerId, historyStorage.maxId, threadId, force); // lol
    }
  }

  public readMessages(peerId: number, msgIds: number[]) {
    msgIds = msgIds.map(mid => this.getServerMessageId(mid));
    if(peerId < 0 && appPeersManager.isChannel(peerId)) {
      const channelId = -peerId;
      apiManager.invokeApi('channels.readMessageContents', {
        channel: appChatsManager.getChannelInput(channelId),
        id: msgIds
      }).then(() => {
        apiUpdatesManager.processUpdateMessage({
          _: 'updateShort',
          update: {
            _: 'updateChannelReadMessagesContents',
            channel_id: channelId,
            messages: msgIds
          }
        });
      });
    } else {
      apiManager.invokeApi('messages.readMessageContents', {
        id: msgIds
      }).then((affectedMessages) => {
        apiUpdatesManager.processUpdateMessage({
          _: 'updateShort',
          update: {
            _: 'updateReadMessagesContents',
            messages: msgIds,
            pts: affectedMessages.pts,
            pts_count: affectedMessages.pts_count
          }
        });
      });
    }
  }

  public getHistoryStorage(peerId: number, threadId?: number) {
    if(threadId) {
      //threadId = this.getLocalMessageId(threadId);
      if(!this.threadsStorage[peerId]) this.threadsStorage[peerId] = {};
      return this.threadsStorage[peerId][threadId] ?? (this.threadsStorage[peerId][threadId] = {count: null, history: new SlicedArray()});
    }

    return this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: new SlicedArray()});
  }

  private handleNotifications = () => {
    window.clearTimeout(this.notificationsHandlePromise);
    this.notificationsHandlePromise = 0;

    //var timeout = $rootScope.idle.isIDLE && StatusManager.isOtherDeviceActive() ? 30000 : 1000;
    //const timeout = 1000;

    for(const _peerId in this.notificationsToHandle) {
      const peerId = +_peerId;
      const notifyPeerToHandle = this.notificationsToHandle[peerId];

      Promise.all([
        appNotificationsManager.getPeerMuted(peerId),
        appNotificationsManager.getNotifySettings(appPeersManager.getInputNotifyPeerById(peerId, true))
      ]).then(([muted, peerTypeNotifySettings]) => {
        const topMessage = notifyPeerToHandle.topMessage;
        if(muted || !topMessage.pFlags.unread) {
          return;
        }

        //setTimeout(() => {
          if(topMessage.pFlags.unread) {
            this.notifyAboutMessage(topMessage, {
              fwdCount: notifyPeerToHandle.fwdCount,
              peerTypeNotifySettings
            });
          }
        //}, timeout);
      });
    }

    this.notificationsToHandle = {};
  };

  public handleUpdate(update: Update) {
    /* if(DEBUG) {
      this.log.debug('handleUpdate', update._, update);
    } */

    switch(update._) {
      case 'updateMessageID': {
        const randomId = update.random_id;
        const pendingData = this.pendingByRandomId[randomId];
        //this.log('AMM updateMessageID:', update, pendingData);
        if(pendingData) {
          const {peerId, tempId, storage} = pendingData;
          //const mid = update.id;
          const mid = this.generateMessageId(update.id);
          const message = this.getMessageFromStorage(storage, mid);
          if(!message.deleted) {
            const historyStorage = this.getHistoryStorage(peerId);
            historyStorage.history.delete(tempId);

            this.finalizePendingMessageCallbacks(storage, tempId, mid);
          } else {
            this.pendingByMessageId[mid] = randomId;
          }
        }

        break;
      }

      case 'updateNewMessage':
      case 'updateNewChannelMessage': {
        const message = update.message as MyMessage;
        const peerId = this.getMessagePeer(message);
        const storage = this.getMessagesStorage(peerId);
        const foundDialog = this.getDialogByPeerId(peerId);

        if(!foundDialog.length) {
          let good = true;
          if(peerId < 0) {
            const chat = appChatsManager.getChat(-peerId);
            if(chat._ === 'channelForbidden' 
              || chat._ === 'chatForbidden' 
              || (chat as Chat.chat).pFlags.left 
              || (chat as Chat.chat).pFlags.kicked 
              || (chat as Chat.chat).pFlags.deactivated) {
              good = false;
            }
          }

          if(good) {
            const set = this.newUpdatesAfterReloadToHandle[peerId] ?? (this.newUpdatesAfterReloadToHandle[peerId] = new Set());
            if(set.has(update)) {
              this.log.error('here we go again', peerId);
              break;
            }

            this.newDialogsToHandle[peerId] = {reload: true};
            this.scheduleHandleNewDialogs();
            this.newUpdatesAfterReloadToHandle[peerId].add(update);
          }

          break;
        }

        /* if(update._ === 'updateNewChannelMessage') {
          const chat = appChatsManager.getChat(-peerId);
          if(chat.pFlags && (chat.pFlags.left || chat.pFlags.kicked)) {
            break;
          }
        } */

        this.saveMessages([message], {storage});
        // this.log.warn(dT(), 'message unread', message.mid, message.pFlags.unread)

        /* if((message as Message.message).grouped_id) {
          this.log('updateNewMessage', message);
        } */

        const pendingMessage = this.checkPendingMessage(message);
        const historyStorage = this.getHistoryStorage(peerId);
        this.updateMessageRepliesIfNeeded(message);

        if(historyStorage.history.findSlice(message.mid)) {
          return false;
        }

        const history = historyStorage.history.slice;
        const topMsgId = history[0];
        history.unshift(message.mid);
        if(message.mid < topMsgId) {
          //this.log.error('this should\'nt have happenned!', message, history);
          history.sort((a, b) => {
            return b - a;
          });
        }
  
        if(historyStorage.count !== null) {
          historyStorage.count++;
        }
  
        if(this.mergeReplyKeyboard(historyStorage, message)) {
          rootScope.broadcast('history_reply_markup', {peerId});
        }
  
        if(message.fromId > 0 && !message.pFlags.out && message.from_id) {
          appUsersManager.forceUserOnline(message.fromId, message.date);
        }

        if(!pendingMessage) {
          if(this.newMessagesToHandle[peerId] === undefined) {
            this.newMessagesToHandle[peerId] = [];
          }
  
          this.newMessagesToHandle[peerId].push(message.mid);
          if(!this.newMessagesHandlePromise) {
            this.newMessagesHandlePromise = window.setTimeout(this.handleNewMessages, 0);
          }
        }
        
        const dialog = foundDialog[0];
        const inboxUnread = !message.pFlags.out && message.pFlags.unread;
        if(dialog) {
          this.setDialogTopMessage(message, dialog);
          if(inboxUnread) {
            dialog.unread_count++;
          }
        }

        if(inboxUnread/*  && ($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE) */) {
          const notifyPeer = message.peerId;
          let notifyPeerToHandle = this.notificationsToHandle[notifyPeer];
          if(notifyPeerToHandle === undefined) {
            notifyPeerToHandle = this.notificationsToHandle[notifyPeer] = {
              fwdCount: 0,
              fromId: 0
            };
          }

          if(notifyPeerToHandle.fromId !== message.fromId) {
            notifyPeerToHandle.fromId = message.fromId;
            notifyPeerToHandle.fwdCount = 0;
          }

          if((message as Message.message).fwd_from) {
            notifyPeerToHandle.fwdCount++;
          }

          notifyPeerToHandle.topMessage = message;

          if(!this.notificationsHandlePromise) {
            this.notificationsHandlePromise = window.setTimeout(this.handleNotifications, 0);
          }
        }

        break;
      }

      case 'updateDialogUnreadMark': {
        //this.log('updateDialogUnreadMark', update);
        const peerId = appPeersManager.getPeerId((update.peer as DialogPeer.dialogPeer).peer);
        const foundDialog = this.getDialogByPeerId(peerId);

        if(!foundDialog.length) {
          this.newDialogsToHandle[peerId] = {reload: true};
          this.scheduleHandleNewDialogs();
        } else {
          const dialog = foundDialog[0];

          if(!update.pFlags.unread) {
            delete dialog.pFlags.unread_mark;
          } else {
            dialog.pFlags.unread_mark = true;
          }

          rootScope.broadcast('dialogs_multiupdate', {peerId: dialog});
        }

        break;
      }

      case 'updateFolderPeers': { // only 0 and 1 folders
        //this.log('updateFolderPeers', update);
        const peers = update.folder_peers;

        this.scheduleHandleNewDialogs();
        peers.forEach((folderPeer) => {
          const {folder_id, peer} = folderPeer;

          const peerId = appPeersManager.getPeerId(peer);
          const dropped = this.dialogsStorage.dropDialog(peerId);
          if(!dropped.length) {
            this.newDialogsToHandle[peerId] = {reload: true};
          } else {
            const dialog = dropped[0];
            this.newDialogsToHandle[peerId] = dialog;

            if(dialog.pFlags?.pinned) {
              delete dialog.pFlags.pinned;
              this.dialogsStorage.pinnedOrders[folder_id].findAndSplice(p => p === dialog.peerId);
            }

            dialog.folder_id = folder_id;

            this.dialogsStorage.generateIndexForDialog(dialog);
            this.dialogsStorage.pushDialog(dialog); // need for simultaneously updatePinnedDialogs
          }
        });
        break;
      }

      case 'updateDialogPinned': {
        const folderId = update.folder_id ?? 0;
        //this.log('updateDialogPinned', update);
        const peerId = appPeersManager.getPeerId((update.peer as DialogPeer.dialogPeer).peer);
        const foundDialog = this.getDialogByPeerId(peerId);

        // этот код внизу никогда не сработает, в папках за пиннед отвечает updateDialogFilter
        /* if(update.folder_id > 1) {
          const filter = this.filtersStorage.filters[update.folder_id];
          if(update.pFlags.pinned) {
            filter.pinned_peers.unshift(peerId);
          } else {
            filter.pinned_peers.findAndSplice(p => p === peerId);
          }
        } */

        this.scheduleHandleNewDialogs();
        if(!foundDialog.length) {
          this.newDialogsToHandle[peerId] = {reload: true};
        } else {
          const dialog = foundDialog[0];
          this.newDialogsToHandle[peerId] = dialog;

          if(!update.pFlags.pinned) {
            delete dialog.pFlags.pinned;
            this.dialogsStorage.pinnedOrders[folderId].findAndSplice(p => p === dialog.peerId);
          } else { // means set
            dialog.pFlags.pinned = true;
          }

          this.dialogsStorage.generateIndexForDialog(dialog);
        } 

        break;
      }

      case 'updatePinnedDialogs': {
        const folderId = update.folder_id ?? 0;
        
        const handleOrder = (order: number[]) => {
          this.dialogsStorage.pinnedOrders[folderId].length = 0;
          let willHandle = false;
          order.reverse(); // index must be higher
          order.forEach((peerId) => {
            newPinned[peerId] = true;
  
            const foundDialog = this.getDialogByPeerId(peerId);
            if(!foundDialog.length) {
              this.newDialogsToHandle[peerId] = {reload: true};
              willHandle = true;
              return;
            }
  
            const dialog = foundDialog[0];
            dialog.pFlags.pinned = true;
            this.dialogsStorage.generateIndexForDialog(dialog);
  
            this.newDialogsToHandle[peerId] = dialog;
            willHandle = true;
          });
          
          this.dialogsStorage.getFolder(folderId).forEach(dialog => {
            const peerId = dialog.peerId;
            if(dialog.pFlags.pinned && !newPinned[peerId]) {
              this.newDialogsToHandle[peerId] = {reload: true};
              willHandle = true;
            }
          });
  
          if(willHandle) {
            this.scheduleHandleNewDialogs();
          }
        };

        //this.log('updatePinnedDialogs', update);
        const newPinned: {[peerId: number]: true} = {};
        if(!update.order) {
          apiManager.invokeApi('messages.getPinnedDialogs', {
            folder_id: folderId
          }).then((dialogsResult) => {
            // * for test reordering and rendering
            // dialogsResult.dialogs.reverse();

            this.applyConversations(dialogsResult);

            handleOrder(dialogsResult.dialogs.map(d => d.peerId));

            /* dialogsResult.dialogs.forEach((dialog) => {
              newPinned[dialog.peerId] = true;
            });

            this.dialogsStorage.getFolder(folderId).forEach((dialog) => {
              const peerId = dialog.peerId;
              if(dialog.pFlags.pinned && !newPinned[peerId]) {
                this.newDialogsToHandle[peerId] = {reload: true};
                this.scheduleHandleNewDialogs();
              }
            }); */
          });

          break;
        }

        //this.log('before order:', this.dialogsStorage[0].map(d => d.peerId));

        handleOrder(update.order.map(peer => appPeersManager.getPeerId((peer as DialogPeer.dialogPeer).peer)));

        break;
      }   

      case 'updateEditMessage':
      case 'updateEditChannelMessage': {
        const message = update.message as MyMessage;
        const peerId = this.getMessagePeer(message);
        const mid = this.generateMessageId(message.id);
        const storage = this.getMessagesStorage(peerId);
        if(storage[mid] === undefined) {
          break;
        }

        // console.trace(dT(), 'edit message', message)
        
        const oldMessage = this.getMessageFromStorage(storage, mid);
        this.saveMessages([message], {storage});
        const newMessage = this.getMessageFromStorage(storage, mid);

        this.handleEditedMessage(oldMessage, newMessage);

        const dialog = this.getDialogByPeerId(peerId)[0];
        const isTopMessage = dialog && dialog.top_message === mid;
        // @ts-ignore
        if(message.clear_history) { // that's will never happen
          if(isTopMessage) {
            rootScope.broadcast('dialog_flush', {peerId});
          }
        } else {
          rootScope.broadcast('message_edit', {
            storage,
            peerId,
            mid
          });

          if(isTopMessage || (message as Message.message).grouped_id) {
            const updatedDialogs: {[peerId: number]: Dialog} = {};
            updatedDialogs[peerId] = dialog;
            rootScope.broadcast('dialogs_multiupdate', updatedDialogs);
          }
        }
        break;
      }

      case 'updateReadChannelDiscussionInbox':
      case 'updateReadChannelDiscussionOutbox':
      case 'updateReadHistoryInbox':
      case 'updateReadHistoryOutbox':
      case 'updateReadChannelInbox':
      case 'updateReadChannelOutbox': {
        const channelId = (update as Update.updateReadChannelInbox).channel_id;
        const maxId = this.generateMessageId((update as Update.updateReadChannelInbox).max_id || (update as Update.updateReadChannelDiscussionInbox).read_max_id);
        const threadId = this.generateMessageId((update as Update.updateReadChannelDiscussionInbox).top_msg_id);
        const peerId = channelId ? -channelId : appPeersManager.getPeerId((update as Update.updateReadHistoryInbox).peer);

        const isOut = update._ === 'updateReadHistoryOutbox' || update._ === 'updateReadChannelOutbox' || update._ === 'updateReadChannelDiscussionOutbox' ? true : undefined;

        const storage = this.getMessagesStorage(peerId);
        const history = getObjectKeysAndSort(storage, 'desc');
        const foundDialog = this.getDialogByPeerId(peerId)[0];
        const stillUnreadCount = (update as Update.updateReadChannelInbox).still_unread_count;
        let newUnreadCount = 0;
        let foundAffected = false;

        //this.log.warn(dT(), 'read', peerId, isOut ? 'out' : 'in', maxId)

        const historyStorage = this.getHistoryStorage(peerId, threadId);

        if(peerId > 0 && isOut) {
          appUsersManager.forceUserOnline(peerId);
        }

        if(threadId) {
          const repliesKey = this.threadsToReplies[peerId + '_' + threadId];
          if(repliesKey) {
            const [peerId, mid] = repliesKey.split('_').map(n => +n);
            this.updateMessage(peerId, mid, 'replies_updated');
          }
        }

        for(let i = 0, length = history.length; i < length; i++) {
          const messageId = history[i];
          if(messageId > maxId) {
            continue;
          }
          
          const message = storage[messageId];

          if(message.pFlags.out !== isOut) {
            continue;
          }

          if(!message.pFlags.unread) {
            break;
          }

          if(threadId) {
            const replyTo = message.reply_to as MessageReplyHeader;
            if(!replyTo || (replyTo.reply_to_top_id || replyTo.reply_to_msg_id) !== threadId) {
              continue;
            }
          }
          
          // this.log.warn('read', messageId, message.pFlags.unread, message)
          if(message.pFlags.unread) {
            delete message.pFlags.unread;
            if(!foundAffected) {
              foundAffected = true;
            }

            if(!message.pFlags.out && !threadId && stillUnreadCount === undefined) {
              newUnreadCount = --foundDialog.unread_count;
            }
            
            appNotificationsManager.cancel('msg' + messageId);
          }
        }

        if(isOut) historyStorage.readOutboxMaxId = maxId;
        else historyStorage.readMaxId = maxId;

        if(!threadId && foundDialog) {
          if(isOut) foundDialog.read_outbox_max_id = maxId;
          else foundDialog.read_inbox_max_id = maxId;

          if(!isOut) {
            if(newUnreadCount < 0 || !this.getReadMaxIdIfUnread(peerId)) {
              foundDialog.unread_count = 0;
            } else if(newUnreadCount && foundDialog.top_message > maxId) {
              foundDialog.unread_count = newUnreadCount;
            }
          }
  
          rootScope.broadcast('dialog_unread', {peerId});
        }

        if(foundAffected) {
          rootScope.broadcast('messages_read');
        }

        if(!threadId && channelId) {
          const threadKeyPart = peerId + '_';
          for(const threadKey in this.threadsToReplies) {
            if(threadKey.indexOf(threadKeyPart) === 0) {
              const [peerId, mid] = this.threadsToReplies[threadKey].split('_').map(n => +n);
              rootScope.broadcast('replies_updated', this.getMessageByPeer(peerId, mid));
            }
          }
        }

        break;
      }

      case 'updateChannelReadMessagesContents':
      case 'updateReadMessagesContents': {
        const channelId = (update as Update.updateChannelReadMessagesContents).channel_id;
        const mids = (update as Update.updateReadMessagesContents).messages.map(id => this.generateMessageId(id));
        const peerId = channelId ? -channelId : this.getMessageById(mids[0]).peerId;
        for(const mid of mids) {
          const message = this.getMessageByPeer(peerId, mid);
          if(!message.deleted) {
            delete message.pFlags.media_unread;
          }
        }

        rootScope.broadcast('messages_media_read', {peerId, mids});
        break;
      }

      case 'updateChannelAvailableMessages': {
        const channelId: number = update.channel_id;
        const messages: number[] = [];
        const peerId: number = -channelId;
        const history = this.getHistoryStorage(peerId).history.slice;
        if(history.length) {
          history.forEach((msgId: number) => {
            if(!update.available_min_id || msgId <= update.available_min_id) {
              messages.push(msgId);
            }
          });
        }

        (update as any as Update.updateDeleteChannelMessages).messages = messages;
      }

      case 'updateDeleteMessages':
      case 'updateDeleteChannelMessages': {
        const channelId: number = (update as Update.updateDeleteChannelMessages).channel_id;
        //const messages = (update as any as Update.updateDeleteChannelMessages).messages;
        const messages = (update as any as Update.updateDeleteChannelMessages).messages.map(id => this.generateMessageId(id));
        const peerId = channelId ? -channelId : this.getMessageById(messages[0]).peerId;
        
        if(!peerId) {
          break;
        }
        
        const historyUpdated = this.handleDeletedMessages(peerId, this.getMessagesStorage(peerId), messages);

        const historyStorage = this.getHistoryStorage(peerId);
        //if(historyStorage !== undefined) {
          for(const mid in historyUpdated.msgs) {
            historyStorage.history.delete(+mid);
          }
          if(historyUpdated.count &&
            historyStorage.count !== null &&
            historyStorage.count > 0) {
            historyStorage.count -= historyUpdated.count;
            if(historyStorage.count < 0) {
              historyStorage.count = 0;
            }
          }

          rootScope.broadcast('history_delete', {peerId, msgs: historyUpdated.msgs});
        //}

        const foundDialog = this.getDialogByPeerId(peerId)[0];
        if(foundDialog) {
          if(historyUpdated.unread) {
            foundDialog.unread_count -= historyUpdated.unread;

            rootScope.broadcast('dialog_unread', {peerId});
          }

          if(historyUpdated.msgs[foundDialog.top_message]) {
            this.reloadConversation(peerId);
          }
        }
        break;
      }

      case 'updateChannel': {
        const channelId: number = update.channel_id;
        const peerId = -channelId;
        const channel = appChatsManager.getChat(channelId);

        const needDialog = channel._ === 'channel' && (!channel.pFlags.left && !channel.pFlags.kicked);
        const foundDialog = this.getDialogByPeerId(peerId);
        const hasDialog = foundDialog.length > 0;

        const canViewHistory = channel._ === 'channel' && (channel.username || !channel.pFlags.left && !channel.pFlags.kicked);
        const hasHistory = this.historiesStorage[peerId] !== undefined;

        if(canViewHistory !== hasHistory) {
          delete this.historiesStorage[peerId];
          rootScope.broadcast('history_forbidden', peerId);
        }

        if(hasDialog !== needDialog) {
          if(needDialog) {
            this.reloadConversation(-channelId);
          } else {
            if(foundDialog[0]) {
              this.dialogsStorage.dropDialog(peerId);
              rootScope.broadcast('dialog_drop', {peerId: peerId, dialog: foundDialog[0]});
            }
          }
        }

        break;
      }

      // @ts-ignore
      case 'updateChannelReload': {
        // @ts-ignore
        const channelId: number = update.channel_id;
        const peerId = -channelId;

        this.dialogsStorage.dropDialog(peerId);

        delete this.historiesStorage[peerId];
        this.reloadConversation(-channelId).then(() => {
          rootScope.broadcast('history_reload', peerId);
        });

        break;
      }

      case 'updateChannelMessageViews': {
        const views = update.views;
        //const mid = update.id;
        const mid = this.generateMessageId(update.id);
        const message = this.getMessageByPeer(-update.channel_id, mid);
        if(!message.deleted && message.views && message.views < views) {
          message.views = views;
          rootScope.broadcast('message_views', {mid, views});
        }
        break;
      }
        
      case 'updateServiceNotification': {
        //this.log('updateServiceNotification', update);
        const fromId = 777000;
        const peerId = fromId;
        const messageId = this.generateTempMessageId(peerId);
        const message: any = {
          _: 'message',
          id: messageId,
          from_id: appPeersManager.getOutputPeer(fromId),
          peer_id: appPeersManager.getOutputPeer(peerId),
          pFlags: {unread: true},
          date: (update.inbox_date || tsNow(true)) + serverTimeManager.serverTimeOffset,
          message: update.message,
          media: update.media,
          entities: update.entities
        };
        if(!appUsersManager.hasUser(fromId)) {
          appUsersManager.saveApiUsers([{
            _: 'user',
            id: fromId,
            pFlags: {verified: true},
            access_hash: 0,
            first_name: 'Telegram',
            phone: '42777'
          }]);
        }
        this.saveMessages([message], {isOutgoing: true});

        if(update.inbox_date) {
          this.pendingTopMsgs[peerId] = messageId;
          this.handleUpdate({
            _: 'updateNewMessage',
            message
          } as any);
        }

        break;
      }

      case 'updatePinnedMessages':
      case 'updatePinnedChannelMessages': {
        const channelId = update._ === 'updatePinnedChannelMessages' ? update.channel_id : undefined;
        const peerId = channelId ? -channelId : appPeersManager.getPeerId((update as Update.updatePinnedMessages).peer);

        /* const storage = this.getSearchStorage(peerId, 'inputMessagesFilterPinned');
        if(storage.count !== storage.history.length) {
          if(storage.count !== undefined) {
            delete this.searchesStorage[peerId]['inputMessagesFilterPinned'];  
          }

          rootScope.broadcast('peer_pinned_messages', peerId);
          break;
        } */

        const messages = update.messages.map(id => this.generateMessageId(id)); 

        const storage = this.getMessagesStorage(peerId);
        const missingMessages = messages.filter(mid => !storage[mid]);
        const getMissingPromise = missingMessages.length ? Promise.all(missingMessages.map(mid => this.wrapSingleMessage(peerId, mid))) : Promise.resolve();
        getMissingPromise.finally(() => {
          const werePinned = update.pFlags?.pinned;
          if(werePinned) {
            for(const mid of messages) {
              //storage.history.push(mid);
              const message = storage[mid];
              message.pFlags.pinned = true;
            }

            /* if(this.pinnedMessages[peerId]?.maxId) {
              const maxMid = Math.max(...messages);
              this.pinnedMessages
            } */

            //storage.history.sort((a, b) => b - a);
          } else {
            for(const mid of messages) {
              //storage.history.findAndSplice(_mid => _mid === mid);
              const message = storage[mid];
              delete message.pFlags.pinned;
            }
          }

          /* const info = this.pinnedMessages[peerId];
          if(info) {
            info.count += messages.length * (werePinned ? 1 : -1);
          } */
  
          delete this.pinnedMessages[peerId];
          appStateManager.getState().then(state => {
            delete state.hiddenPinnedMessages[peerId];
            rootScope.broadcast('peer_pinned_messages', {peerId, mids: messages, pinned: werePinned});
          });
        });

        break;
      }

      case 'updateNotifySettings': {
        const {peer, notify_settings} = update;
        if(peer._ === 'notifyPeer') {
          const peerId = appPeersManager.getPeerId((peer as NotifyPeer.notifyPeer).peer);
        
          const dialog = this.getDialogByPeerId(peerId)[0];
          if(dialog) {
            dialog.notify_settings = notify_settings;
            rootScope.broadcast('dialog_notify_settings', dialog);
          }
        }

        /////this.log('updateNotifySettings', peerId, notify_settings);
        break;
      }

      case 'updateNewScheduledMessage': {
        const message = update.message as MyMessage;
        const peerId = this.getMessagePeer(message);

        const storage = this.scheduledMessagesStorage[peerId];
        if(storage) {
          const mid = this.generateMessageId(message.id);

          const oldMessage = this.getMessageFromStorage(storage, mid);
          this.saveMessages([message], {storage, isScheduled: true});
          const newMessage = this.getMessageFromStorage(storage, mid);

          if(!oldMessage.deleted) {
            this.handleEditedMessage(oldMessage, newMessage);
            rootScope.broadcast('message_edit', {storage, peerId, mid: message.mid});
          } else {
            const pendingMessage = this.checkPendingMessage(message);
            if(!pendingMessage) {
              rootScope.broadcast('scheduled_new', {peerId, mid: message.mid});
            }
          }
        }
        
        break;
      }

      case 'updateDeleteScheduledMessages': {
        const peerId = appPeersManager.getPeerId(update.peer);

        const storage = this.scheduledMessagesStorage[peerId];
        if(storage) {
          const mids = update.messages.map(id => this.generateMessageId(id));
          this.handleDeletedMessages(peerId, storage, mids);

          rootScope.broadcast('scheduled_delete', {peerId, mids});
        }

        break;
      }
    }
  }

  private updateMessageRepliesIfNeeded(threadMessage: MyMessage) {
    try { // * на всякий случай, скорее всего это не понадобится
      if(threadMessage.peerId < 0 && threadMessage.reply_to) {
        const threadId = threadMessage.reply_to.reply_to_top_id || threadMessage.reply_to.reply_to_msg_id;
        const threadKey = threadMessage.peerId + '_' + threadId;
        const repliesKey = this.threadsToReplies[threadKey];
        if(repliesKey) {
          const [peerId, mid] = repliesKey.split('_').map(n => +n);

          this.updateMessage(peerId, mid, 'replies_updated');
        }
      }
    } catch(err) {
      this.log.error('incrementMessageReplies err', err, threadMessage);
    }
  }

  public updateMessage(peerId: number, mid: number, broadcastEventName?: 'replies_updated'): Promise<Message.message> {
    const promise: Promise<Message.message> = this.wrapSingleMessage(peerId, mid, true).then(() => {
      const message = this.getMessageByPeer(peerId, mid);

      if(broadcastEventName) {
        rootScope.broadcast(broadcastEventName, message);
      }

      return message;
    });
    
    return promise;
  }

  private checkPendingMessage(message: any) {
    const randomId = this.pendingByMessageId[message.mid];
    let pendingMessage: any;
    if(randomId) {
      const pendingData = this.pendingByRandomId[randomId];
      if(pendingMessage = this.finalizePendingMessage(randomId, message)) {
        rootScope.broadcast('history_update', {storage: pendingData.storage, peerId: message.peerId, mid: message.mid});
      }

      delete this.pendingByMessageId[message.mid];
    }

    return pendingMessage;
  }

  public mutePeer(peerId: number) {
    const settings: InputPeerNotifySettings = {
      _: 'inputPeerNotifySettings'
    };

    const dialog = appMessagesManager.getDialogByPeerId(peerId)[0];
    let muted = true;
    if(dialog && dialog.notify_settings) {
      muted = dialog.notify_settings.mute_until > (Date.now() / 1000 | 0);
    }
    
    if(!muted) {
      settings.mute_until = 2147483647;
    }

    return appNotificationsManager.updateNotifySettings({
      _: 'inputNotifyPeer',
      peer: appPeersManager.getInputPeerById(peerId)
    }, settings);
  }

  public canWriteToPeer(peerId: number) {
    if(peerId < 0) {
      const isChannel = appPeersManager.isChannel(peerId);
      const hasRights = isChannel && appChatsManager.hasRights(-peerId, 'send_messages'); 
      return !isChannel || hasRights;
    } else {
      return appUsersManager.canSendToUser(peerId);
    }
  }

  public finalizePendingMessage(randomId: string, finalMessage: any) {
    const pendingData = this.pendingByRandomId[randomId];
    // this.log('pdata', randomID, pendingData)

    if(pendingData) {
      const {peerId, tempId, storage} = pendingData;
      const historyStorage = this.getHistoryStorage(peerId);

      // this.log('pending', randomID, historyStorage.pending)
      historyStorage.history.delete(tempId);

      const message = this.getMessageFromStorage(storage, tempId);
      if(!message.deleted) {
        delete message.pFlags.is_outgoing;
        delete message.pending;
        delete message.error;
        delete message.random_id;
        delete message.send;

        rootScope.broadcast('messages_pending');
      }
      
      delete this.pendingByRandomId[randomId];

      this.finalizePendingMessageCallbacks(storage, tempId, finalMessage.mid);

      return message;
    }

    return false;
  }

  public finalizePendingMessageCallbacks(storage: MessagesStorage, tempId: number, mid: number) {
    const message = this.getMessageFromStorage(storage, mid);
    const callbacks = this.tempFinalizeCallbacks[tempId];
    //this.log.warn(callbacks, tempId);
    if(callbacks !== undefined) {
      for(const name in callbacks) {
        const {deferred, callback} = callbacks[name];
        //this.log(`finalizePendingMessageCallbacks: will invoke ${name} callback`);
        callback(message).then(deferred.resolve, deferred.reject);
      }

      delete this.tempFinalizeCallbacks[tempId];
    }

    // set cached url to media
    if(message.media) {
      if(message.media.photo) {
        const photo = appPhotosManager.getPhoto('' + tempId);
        if(/* photo._ !== 'photoEmpty' */photo) {
          const newPhoto = message.media.photo as MyPhoto;
          // костыль
          defineNotNumerableProperties(newPhoto, ['downloaded', 'url']);
          newPhoto.downloaded = photo.downloaded;
          newPhoto.url = photo.url;

          const photoSize = newPhoto.sizes[newPhoto.sizes.length - 1] as PhotoSize.photoSize;
          defineNotNumerableProperties(photoSize, ['url']);
          photoSize.url = photo.url;

          const downloadOptions = appPhotosManager.getPhotoDownloadOptions(newPhoto, photoSize);
          const fileName = getFileNameByLocation(downloadOptions.location);
          appDownloadManager.fakeDownload(fileName, photo.url);
        }
      } else if(message.media.document) {
        const doc = appDocsManager.getDoc('' + tempId);
        if(doc) {
          if(/* doc._ !== 'documentEmpty' &&  */doc.type && doc.type !== 'sticker') {
            const newDoc = message.media.document;
            newDoc.downloaded = doc.downloaded;
            newDoc.url = doc.url;

            const fileName = appDocsManager.getInputFileName(newDoc);
            appDownloadManager.fakeDownload(fileName, doc.url);
          }
        }
      } else if(message.media.poll) {
        delete appPollsManager.polls[tempId];
        delete appPollsManager.results[tempId];
      }
    }

    const tempMessage = this.getMessageFromStorage(storage, tempId);
    delete storage[tempId];

    rootScope.broadcast('message_sent', {storage, tempId, tempMessage, mid});
  }

  public incrementMaxSeenId(maxId: number) {
    if(!maxId || !(!this.maxSeenId || maxId > this.maxSeenId)) {
      return false;
    }

    this.maxSeenId = maxId;

    apiManager.invokeApi('messages.receivedMessages', {
      max_id: this.getServerMessageId(maxId)
    });
  }

  private notifyAboutMessage(message: MyMessage, options: Partial<{
    fwdCount: number,
    peerTypeNotifySettings: PeerNotifySettings
  }> = {}) {
    const peerId = this.getMessagePeer(message);
    let peerString: string;
    const notification: NotifyOptions = {};
    var notificationMessage: string,
      notificationPhoto: any;

    const _ = (str: string) => str;

    const localSettings = appNotificationsManager.getLocalSettings();
    if(options.peerTypeNotifySettings.show_previews) {
      if(message._ === 'message' && message.fwd_from && options.fwdCount) {
        notificationMessage = 'Forwarded ' + options.fwdCount + ' messages';//fwdMessagesPluralize(options.fwd_count);
      } else {
        notificationMessage = this.wrapMessageForReply(message, undefined, undefined, true);
      }
    } else {
      notificationMessage = 'New notification';
    }

    if(peerId > 0) {
      const fromUser = appUsersManager.getUser(message.fromId);
      const fromPhoto = appUsersManager.getUserPhoto(message.fromId);

      notification.title = (fromUser.first_name || '') +
        (fromUser.first_name && fromUser.last_name ? ' ' : '') +
        (fromUser.last_name || '');
      if(!notification.title) {
        notification.title = fromUser.phone || _('conversation_unknown_user_raw');
      }

      notificationPhoto = fromPhoto;

      peerString = appUsersManager.getUserString(peerId);
    } else {
      notification.title = appChatsManager.getChat(-peerId).title || _('conversation_unknown_chat_raw');

      if(message.fromId) {
        var fromUser = appUsersManager.getUser(message.fromId);
        notification.title = (fromUser.first_name || fromUser.last_name || _('conversation_unknown_user_raw')) +
        ' @ ' +
        notification.title;
      }

      notificationPhoto = appChatsManager.getChatPhoto(-peerId);

      peerString = appChatsManager.getChatString(-peerId);
    }

    notification.title = RichTextProcessor.wrapPlainText(notification.title);

    notification.onclick = () => {
      /* rootScope.broadcast('history_focus', {
        peerString: peerString,
        messageID: message.flags & 16 ? message.mid : 0
      }); */
    };

    notification.message = notificationMessage;
    notification.key = 'msg' + message.mid;
    notification.tag = peerString;
    notification.silent = true;//message.pFlags.silent || false;

    /* if(notificationPhoto.location && !notificationPhoto.location.empty) {
      apiManager.downloadSmallFile(notificationPhoto.location, notificationPhoto.size).then(function (blob) {
        if (message.pFlags.unread) {
          notification.image = blob
          NotificationsManager.notify(notification)
        }
      })
    } else { */
      appNotificationsManager.notify(notification);
    //}
  }

  public getScheduledMessagesStorage(peerId: number) {
    return this.scheduledMessagesStorage[peerId] ?? (this.scheduledMessagesStorage[peerId] = this.createMessageStorage());
  }

  public getScheduledMessages(peerId: number): Promise<number[]> {
    if(!this.canWriteToPeer(peerId)) return Promise.resolve([]);

    const storage = this.getScheduledMessagesStorage(peerId);
    if(Object.keys(storage).length) {
      return Promise.resolve(Object.keys(storage).map(id => +id));
    }

    return apiManager.invokeApi('messages.getScheduledHistory', {
      peer: appPeersManager.getInputPeerById(peerId),
      hash: 0
    }).then(historyResult => {
      if(historyResult._ !== 'messages.messagesNotModified') {
        appUsersManager.saveApiUsers(historyResult.users);
        appChatsManager.saveApiChats(historyResult.chats);
        
        const storage = this.getScheduledMessagesStorage(peerId);
        this.saveMessages(historyResult.messages, {storage, isScheduled: true});
        return Object.keys(storage).map(id => +id);
      }
      
      return [];
    });
  }

  public sendScheduledMessages(peerId: number, mids: number[]) {
    return apiManager.invokeApi('messages.sendScheduledMessages', {
      peer: appPeersManager.getInputPeerById(peerId),
      id: mids.map(mid => this.getServerMessageId(mid))
    }).then(updates => {
      apiUpdatesManager.processUpdateMessage(updates);
    });
  }

  public deleteScheduledMessages(peerId: number, mids: number[]) {
    return apiManager.invokeApi('messages.deleteScheduledMessages', {
      peer: appPeersManager.getInputPeerById(peerId),
      id: mids.map(mid => this.getServerMessageId(mid))
    }).then(updates => {
      apiUpdatesManager.processUpdateMessage(updates);
    });
  }

  /**
   * * https://core.telegram.org/api/offsets, offset_id is inclusive
   */
  public getHistory(peerId: number, maxId = 0, limit: number, backLimit?: number, threadId?: number): Promise<HistoryResult> | HistoryResult {
    const historyStorage = this.getHistoryStorage(peerId, threadId);

    let offset = 0;
    /* 
    let offsetFound = true;

    if(maxId) {
      offsetFound = false;
      for(; offset < historyStorage.history.length; offset++) {
        if(maxId > historyStorage.history.slice[offset]) {
          offsetFound = true;
          break;
        }
      }
    }

    if(offsetFound && (
      historyStorage.count !== null && historyStorage.history.length === historyStorage.count ||
      historyStorage.history.length >= offset + limit
      )) {
      if(backLimit) {
        backLimit = Math.min(offset, backLimit);
        offset = Math.max(0, offset - backLimit);
        limit += backLimit;
      } else {
        limit = limit;
      }

      const history = historyStorage.history.slice.slice(offset, offset + limit);
      return {
        count: historyStorage.count,
        history: history,
        offsetIdOffset: offset
      };
    }

    if(offsetFound) {
      offset = 0;
    } */

    if(backLimit) {
      offset = -backLimit;
      limit += backLimit;

      /* return this.requestHistory(reqPeerId, maxId, limit, offset, undefined, threadId).then((historyResult) => {
        historyStorage.count = (historyResult as MessagesMessages.messagesMessagesSlice).count || historyResult.messages.length;

        const history = (historyResult.messages as MyMessage[]).map(message => message.mid);
        return {
          count: historyStorage.count,
          history,
          offsetIdOffset: (historyResult as MessagesMessages.messagesMessagesSlice).offset_id_offset || 0
        };
      }); */
    }

    const haveSlice = historyStorage.history.sliceMe(maxId, offset, limit);
    if(haveSlice && (haveSlice.slice.length === limit || (haveSlice.fulfilled & SliceEnd.Both))) {
      return {
        count: historyStorage.count,
        history: haveSlice.slice,
        offsetIdOffset: haveSlice.offsetIdOffset
      }; 
    }

    return this.fillHistoryStorage(peerId, maxId, limit, offset, historyStorage, threadId).then(() => {
      const slice = historyStorage.history.sliceMe(maxId, offset, limit);
      return {
        count: historyStorage.count,
        history: slice?.slice || historyStorage.history.constructSlice(),
        offsetIdOffset: slice?.offsetIdOffset || historyStorage.count
      };
    });
  }

  public fillHistoryStorage(peerId: number, offset_id: number, limit: number, add_offset: number, historyStorage: HistoryStorage, threadId?: number): Promise<void> {
    return this.requestHistory(peerId, offset_id, limit, add_offset, undefined, threadId).then((historyResult) => {
      historyStorage.count = (historyResult as MessagesMessages.messagesMessagesSlice).count || historyResult.messages.length;

      const offsetIdOffset = (historyResult as MessagesMessages.messagesMessagesSlice).offset_id_offset || 0;
      const isTopEnd = offsetIdOffset >= (historyStorage.count - limit) || historyStorage.count < (limit + add_offset);

      /* if(!maxId && historyResult.messages.length) {
        maxId = this.incrementMessageId((historyResult.messages[0] as MyMessage).mid, 1);
      }

      const wasTotalCount = historyStorage.history.length; */

      historyResult.messages.forEach((message) => {
        if(this.mergeReplyKeyboard(historyStorage, message)) {
          rootScope.broadcast('history_reply_markup', {peerId});
        }
      });

      const mids = historyResult.messages.map((message) => (message as MyMessage).mid);
      // * add bound manually. 
      // * offset_id will be inclusive only if there is 'add_offset' <= -1 (-1 - will only include the 'offset_id')
      if(offset_id && !mids.includes(offset_id) && offsetIdOffset < historyStorage.count) {
        let i = 0;
        for(const length = mids.length; i < length; ++i) {
          if(offset_id > mids[i]) {
            break;
          }
        }

        mids.splice(i, 0, offset_id);
      }
      
      historyStorage.history.insertSlice(mids);

      if(isTopEnd) {
        historyStorage.history.last.setEnd(SliceEnd.Top);
      }
      
      /* const isBackLimit = offset < 0 && -offset !== fullLimit;
      if(isBackLimit) {
        return;
      }

      const totalCount = historyStorage.history.length;
      fullLimit -= (totalCount - wasTotalCount);

      const migratedNextPeer = this.migratedFromTo[peerId];
      const migratedPrevPeer = this.migratedToFrom[peerId]
      const isMigrated = migratedNextPeer !== undefined || migratedPrevPeer !== undefined;

      if(isMigrated) {
        historyStorage.count = Math.max(historyStorage.count, totalCount) + 1;
      }

      if(fullLimit > 0) {
        maxId = historyStorage.history.slice[totalCount - 1];
        if(isMigrated) {
          if(!historyResult.messages.length) {
            if(migratedPrevPeer) {
              maxId = 0;
              peerId = migratedPrevPeer;
            } else {
              historyStorage.count = totalCount;
              return true;
            }
          }

          return this.fillHistoryStorage(peerId, maxId, fullLimit, historyStorage, threadId);
        } else if(totalCount < historyStorage.count) {
          return this.fillHistoryStorage(peerId, maxId, fullLimit, offset, historyStorage, threadId);
        }
      } */
    });
  }

  public requestHistory(peerId: number, maxId: number, limit = 0, offset = 0, offsetDate = 0, threadId = 0): Promise<Exclude<MessagesMessages, MessagesMessages.messagesMessagesNotModified>> {
    //console.trace('requestHistory', peerId, maxId, limit, offset);

    //rootScope.broadcast('history_request');

    const options: any = {
      peer: appPeersManager.getInputPeerById(peerId),
      offset_id: this.getServerMessageId(maxId) || 0,
      offset_date: offsetDate,
      add_offset: offset,
      limit,
      max_id: 0,
      min_id: 0,
      hash: 0
    };

    if(threadId) {
      options.msg_id = this.getServerMessageId(threadId) || 0;
    }

    const promise: ReturnType<AppMessagesManager['requestHistory']> = apiManager.invokeApi(threadId ? 'messages.getReplies' : 'messages.getHistory', options, {
      //timeout: APITIMEOUT,
      noErrorBox: true
    }) as any;

    return promise.then((historyResult) => {
      if(DEBUG) {
        this.log('requestHistory result:', peerId, historyResult, maxId, limit, offset);
      }

      appUsersManager.saveApiUsers(historyResult.users);
      appChatsManager.saveApiChats(historyResult.chats);
      this.saveMessages(historyResult.messages);

      if(appPeersManager.isChannel(peerId)) {
        apiUpdatesManager.addChannelState(-peerId, (historyResult as MessagesMessages.messagesChannelMessages).pts);
      }

      let length = historyResult.messages.length;
      if(length && historyResult.messages[length - 1].deleted) {
        historyResult.messages.splice(length - 1, 1);
        length--;
        (historyResult as MessagesMessages.messagesMessagesSlice).count--;
      }

      // will load more history if last message is album grouped (because it can be not last item)
      const historyStorage = this.getHistoryStorage(peerId, threadId);
      // historyResult.messages: desc sorted
      if(length && (historyResult.messages[length - 1] as Message.message).grouped_id 
        && (historyStorage.history.length + historyResult.messages.length) < (historyResult as MessagesMessages.messagesMessagesSlice).count) {
        return this.requestHistory(peerId, (historyResult.messages[length - 1] as Message.message).mid, 10, 0, offsetDate, threadId).then((_historyResult) => {
          return historyResult;
        });
      }

      return historyResult;
    }, (error) => {
      switch (error.type) {
        case 'CHANNEL_PRIVATE':
          let channel = appChatsManager.getChat(-peerId);
          channel = {_: 'channelForbidden', access_hash: channel.access_hash, title: channel.title};
          apiUpdatesManager.processUpdateMessage({
            _: 'updates',
            updates: [{
              _: 'updateChannel',
              channel_id: -peerId
            }],
            chats: [channel],
            users: []
          });
          break;
      }

      throw error;
    });
  }

  public fetchSingleMessages() {
    if(this.fetchSingleMessagesPromise) {
      return this.fetchSingleMessagesPromise;
    }

    return this.fetchSingleMessagesPromise = new Promise((resolve) => {
      setTimeout(() => {
        let promises: Promise<void>[] = [];
        
        for(const peerId in this.needSingleMessages) {
          const mids = this.needSingleMessages[peerId];
          delete this.needSingleMessages[peerId];
    
          const msgIds: InputMessage[] = mids.map((msgId: number) => {
            return {
              _: 'inputMessageID',
              id: this.getServerMessageId(msgId)
            };
          });
    
          let promise: Promise<MethodDeclMap['channels.getMessages']['res'] | MethodDeclMap['messages.getMessages']['res']>;
          if(+peerId < 0 && appPeersManager.isChannel(+peerId)) {
            promise = apiManager.invokeApi('channels.getMessages', {
              channel: appChatsManager.getChannelInput(-+peerId),
              id: msgIds
            });
          } else {
            promise = apiManager.invokeApi('messages.getMessages', {
              id: msgIds
            });
          }
    
          promises.push(promise.then(getMessagesResult => {
            if(getMessagesResult._ !== 'messages.messagesNotModified') {
              appUsersManager.saveApiUsers(getMessagesResult.users);
              appChatsManager.saveApiChats(getMessagesResult.chats);
              this.saveMessages(getMessagesResult.messages);
            }
    
            rootScope.broadcast('messages_downloaded', {peerId: +peerId, mids});
          }));
        }

        Promise.all(promises).finally(() => {
          this.fetchSingleMessagesPromise = null;
          if(Object.keys(this.needSingleMessages).length) this.fetchSingleMessages();
          resolve();
        });
      }, 0);
    });
  }

  public wrapSingleMessage(peerId: number, msgId: number, overwrite = false): Promise<void> {
    if(!this.getMessageByPeer(peerId, msgId).deleted && !overwrite) {
      rootScope.broadcast('messages_downloaded', {peerId, mids: [msgId]});
      return Promise.resolve();
    } else if(!this.needSingleMessages[peerId] || this.needSingleMessages[peerId].indexOf(msgId) === -1) {
      (this.needSingleMessages[peerId] ?? (this.needSingleMessages[peerId] = [])).push(msgId);
      return this.fetchSingleMessages();
    } else if(this.fetchSingleMessagesPromise) {
      return this.fetchSingleMessagesPromise;
    }
  }

  public setTyping(peerId: number, _action: any): Promise<boolean> {
    if(!rootScope.myId || !peerId || !this.canWriteToPeer(peerId) || peerId === rootScope.myId) return Promise.resolve(false);
    
    const action: SendMessageAction = typeof(_action) === 'string' ? {_: _action} : _action;
    return apiManager.invokeApi('messages.setTyping', {
      peer: appPeersManager.getInputPeerById(peerId),
      action
    }) as Promise<boolean>;
  }

  private handleDeletedMessages(peerId: number, storage: MessagesStorage, messages: number[]) {
    const history: {
      count: number, 
      unread: number, 
      msgs: {[mid: number]: true},
      albums?: {[groupId: string]: Set<number>},
    } = {count: 0, unread: 0, msgs: {}} as any;

    for(const mid of messages) {
      const message: MyMessage = this.getMessageFromStorage(storage, mid);
      if(message.deleted) continue;

      if((message as Message.message).media) {
        // @ts-ignore
        const c = message.media.webpage || message.media;
        const smth = c.photo || c.document;

        if(smth?.file_reference) {
          referenceDatabase.deleteContext(smth.file_reference, {type: 'message', peerId, messageId: mid});
        }

        // @ts-ignore
        if(message.media.webpage) {
          // @ts-ignore
          appWebPagesManager.deleteWebPageFromPending(message.media.webpage, mid);
        }
      }

      this.updateMessageRepliesIfNeeded(message);

      if(!message.pFlags.out && !message.pFlags.is_outgoing && message.pFlags.unread) {
        history.unread++;
        appNotificationsManager.cancel('msg' + mid);
      }
      history.count++;
      history.msgs[mid] = true;

      message.deleted = true;

      if(message._ !== 'messageService' && message.grouped_id) {
        const groupedStorage = this.groupedMessagesStorage[message.grouped_id];
        if(groupedStorage) {
          delete groupedStorage[mid];

          if(!history.albums) history.albums = {};
          (history.albums[message.grouped_id] || (history.albums[message.grouped_id] = new Set())).add(mid);

          if(!Object.keys(groupedStorage).length) {
            delete history.albums;
            delete this.groupedMessagesStorage[message.grouped_id];
          }
        }
      }

      delete storage[mid];

      const peerMessagesToHandle = this.newMessagesToHandle[peerId];
      if(peerMessagesToHandle && peerMessagesToHandle.length) {
        const peerMessagesHandlePos = peerMessagesToHandle.indexOf(mid);
        if(peerMessagesHandlePos !== -1) {
          peerMessagesToHandle.splice(peerMessagesHandlePos);
        }
      }
    }

    if(history.albums) {
      for(const groupId in history.albums) {
        rootScope.broadcast('album_edit', {peerId, groupId, deletedMids: [...history.albums[groupId]]});
        /* const mids = this.getMidsByAlbum(groupId);
        if(mids.length) {
          const mid = Math.max(...mids);
          rootScope.$broadcast('message_edit', {peerId, mid, justMedia: false});
        } */
      }
    }

    return history;
  }
  
  private handleEditedMessage(oldMessage: any, newMessage: any) {
    if(oldMessage.media?.webpage) {
      appWebPagesManager.deleteWebPageFromPending(oldMessage.media.webpage, oldMessage.mid);
    }
  }
}

const appMessagesManager = new AppMessagesManager();
MOUNT_CLASS_TO.appMessagesManager = appMessagesManager;
export default appMessagesManager;
