From 5505ef5b8b908dbd3bba2f95ea377b8e96c2d222 Mon Sep 17 00:00:00 2001
From: morethanwords <thanwords24@gmail.com>
Date: Tue, 18 May 2021 17:17:54 +0300
Subject: [PATCH] Chat input autocomplete helper

Fix iOS 13 blank page
---
 src/components/appSearchSuper..ts             |   4 +-
 src/components/chat/chat.ts                   |  23 +-
 src/components/chat/emojiHelper.ts            |  58 ++++
 src/components/chat/input.ts                  | 288 +++++++++++-------
 .../emoticonsDropdown/tabs/emoji.ts           | 189 ++++++------
 src/components/emoticonsDropdown/tabs/gifs.ts |   4 +-
 .../emoticonsDropdown/tabs/stickers.ts        |  16 +-
 src/helpers/cleanSearchText.ts                |  33 ++
 src/helpers/cleanUsername.ts                  |  14 +
 src/helpers/dom/attachListNavigation.ts       |  12 +-
 src/helpers/dom/renderImageFromUrl.ts         |   2 +-
 src/lib/appManagers/appEmojiManager.ts        | 185 +++++++++++
 src/lib/appManagers/appImManager.ts           |  21 +-
 src/lib/appManagers/appUsersManager.ts        |  24 +-
 src/lib/mtproto/networkerFactory.ts           |  12 +
 src/lib/mtproto/singleInstance.ts             | 113 +++++++
 src/lib/richtextprocessor.ts                  |   2 +-
 src/lib/rootScope.ts                          |  35 ++-
 .../{searchIndexManager.ts => searchIndex.ts} |  77 ++---
 src/lib/sessionStorage.ts                     |   2 +
 src/lib/storages/dialogs.ts                   |  14 +-
 src/scss/partials/_chatEmojiHelper.scss       |  19 ++
 src/scss/partials/_emojiDropdown.scss         |  59 ----
 src/scss/style.scss                           |  60 ++++
 24 files changed, 882 insertions(+), 384 deletions(-)
 create mode 100644 src/components/chat/emojiHelper.ts
 create mode 100644 src/helpers/cleanSearchText.ts
 create mode 100644 src/helpers/cleanUsername.ts
 create mode 100644 src/lib/appManagers/appEmojiManager.ts
 create mode 100644 src/lib/mtproto/singleInstance.ts
 rename src/lib/{searchIndexManager.ts => searchIndex.ts} (56%)
 create mode 100644 src/scss/partials/_chatEmojiHelper.scss

diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts
index 317e8e28..a956422e 100644
--- a/src/components/appSearchSuper..ts
+++ b/src/components/appSearchSuper..ts
@@ -17,7 +17,6 @@ import appUsersManager from "../lib/appManagers/appUsersManager";
 import { logger } from "../lib/logger";
 import RichTextProcessor from "../lib/richtextprocessor";
 import rootScope from "../lib/rootScope";
-import searchIndexManager from "../lib/searchIndexManager";
 import AppMediaViewer from "./appMediaViewer";
 import { SearchGroup, SearchGroupType } from "./appSearch";
 import { horizontalMenu } from "./horizontalMenu";
@@ -39,6 +38,7 @@ import appSidebarRight from "./sidebarRight";
 import mediaSizes from "../helpers/mediaSizes";
 import appImManager from "../lib/appManagers/appImManager";
 import positionElementByIndex from "../helpers/dom/positionElementByIndex";
+import cleanSearchText from "../helpers/cleanSearchText";
 
 //const testScroll = false;
 
@@ -738,7 +738,7 @@ export default class AppSearchSuper {
           });
   
           if(showMembersCount && (peer.participants_count || peer.participants)) {
-            const regExp = new RegExp(`(${escapeRegExp(query)}|${escapeRegExp(searchIndexManager.cleanSearchText(query))})`, 'gi');
+            const regExp = new RegExp(`(${escapeRegExp(query)}|${escapeRegExp(cleanSearchText(query))})`, 'gi');
             dom.titleSpan.innerHTML = dom.titleSpan.innerHTML.replace(regExp, '<i>$1</i>');
             dom.lastMessageSpan.append(appChatsManager.getChatMembersString(-peerId));
           } else if(peerId === rootScope.myId) {
diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts
index 3ccf5f69..2c42ce4f 100644
--- a/src/components/chat/chat.ts
+++ b/src/components/chat/chat.ts
@@ -19,6 +19,7 @@ import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
 import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager";
 import type { ApiManagerProxy } from "../../lib/mtproto/mtprotoworker";
 import type { AppDraftsManager } from "../../lib/appManagers/appDraftsManager";
+import type { AppEmojiManager } from "../../lib/appManagers/appEmojiManager";
 import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager";
 import type sessionStorage from '../../lib/sessionStorage';
 import EventListenerBase from "../../helpers/eventListenerBase";
@@ -64,7 +65,25 @@ export default class Chat extends EventListenerBase<{
 
   public noAutoDownloadMedia: boolean;
   
-  constructor(public appImManager: AppImManager, public appChatsManager: AppChatsManager, public appDocsManager: AppDocsManager, public appInlineBotsManager: AppInlineBotsManager, public appMessagesManager: AppMessagesManager, public appPeersManager: AppPeersManager, public appPhotosManager: AppPhotosManager, public appProfileManager: AppProfileManager, public appStickersManager: AppStickersManager, public appUsersManager: AppUsersManager, public appWebPagesManager: AppWebPagesManager, public appPollsManager: AppPollsManager, public apiManager: ApiManagerProxy, public appDraftsManager: AppDraftsManager, public serverTimeManager: ServerTimeManager, public storage: typeof sessionStorage, public appNotificationsManager: AppNotificationsManager) {
+  constructor(public appImManager: AppImManager, 
+    public appChatsManager: AppChatsManager, 
+    public appDocsManager: AppDocsManager, 
+    public appInlineBotsManager: AppInlineBotsManager, 
+    public appMessagesManager: AppMessagesManager, 
+    public appPeersManager: AppPeersManager, 
+    public appPhotosManager: AppPhotosManager, 
+    public appProfileManager: AppProfileManager, 
+    public appStickersManager: AppStickersManager, 
+    public appUsersManager: AppUsersManager, 
+    public appWebPagesManager: AppWebPagesManager, 
+    public appPollsManager: AppPollsManager, 
+    public apiManager: ApiManagerProxy, 
+    public appDraftsManager: AppDraftsManager, 
+    public serverTimeManager: ServerTimeManager, 
+    public storage: typeof sessionStorage, 
+    public appNotificationsManager: AppNotificationsManager,
+    public appEmojiManager: AppEmojiManager
+  ) {
     super();
 
     this.container = document.createElement('div');
@@ -150,7 +169,7 @@ export default class Chat extends EventListenerBase<{
 
     this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appNotificationsManager);
     this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appDocsManager, this.appPeersManager, this.appChatsManager, this.storage);
-    this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager, this.appNotificationsManager);
+    this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager, this.appNotificationsManager, this.appEmojiManager);
     this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager);
     this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager);
 
diff --git a/src/components/chat/emojiHelper.ts b/src/components/chat/emojiHelper.ts
new file mode 100644
index 00000000..f821c921
--- /dev/null
+++ b/src/components/chat/emojiHelper.ts
@@ -0,0 +1,58 @@
+import type ChatInput from "./input";
+import attachListNavigation from "../../helpers/dom/attachlistNavigation";
+import { appendEmoji, getEmojiFromElement } from "../emoticonsDropdown/tabs/emoji";
+import { ScrollableX } from "../scrollable";
+import AutocompleteHelper from "./autocompleteHelper";
+
+export default class EmojiHelper extends AutocompleteHelper {
+  private emojisContainer: HTMLDivElement;
+  private scrollable: ScrollableX;
+
+  constructor(appendTo: HTMLElement, private chatInput: ChatInput) {
+    super(appendTo);
+
+    this.container.classList.add('emoji-helper');
+
+    this.addEventListener('visible', () => {
+      const list = this.emojisContainer;
+      const {detach} = attachListNavigation({
+        list, 
+        type: 'x',
+        onSelect: (target) => {
+          this.chatInput.onEmojiSelected(getEmojiFromElement(target as any), true);
+        },
+        once: true
+      });
+
+      this.addEventListener('hidden', () => {
+        list.innerHTML = '';
+        detach();
+      }, true);
+    });
+  }
+
+  private init() {
+    this.emojisContainer = document.createElement('div');
+    this.emojisContainer.classList.add('emoji-helper-emojis', 'super-emojis');
+
+    this.container.append(this.emojisContainer);
+
+    this.scrollable = new ScrollableX(this.container);
+  }
+
+  public renderEmojis(emojis: string[]) {
+    if(this.init) {
+      this.init();
+      this.init = null;
+    }
+
+    if(emojis.length) {
+      this.emojisContainer.innerHTML = '';
+      emojis.forEach(emoji => {
+        appendEmoji(emoji, this.emojisContainer);
+      });
+    }
+
+    this.toggle(!emojis.length);
+  }
+}
diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts
index f49f5e46..c8c40247 100644
--- a/src/components/chat/input.ts
+++ b/src/components/chat/input.ts
@@ -12,6 +12,7 @@ import type { AppPeersManager } from '../../lib/appManagers/appPeersManager';
 import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager";
 import type { AppImManager } from '../../lib/appManagers/appImManager';
 import type { AppDraftsManager, MyDraftMessage } from '../../lib/appManagers/appDraftsManager';
+import type { AppEmojiManager } from '../../lib/appManagers/appEmojiManager';
 import type { ServerTimeManager } from '../../lib/mtproto/serverTimeManager';
 import type Chat from './chat';
 import Recorder from '../../../public/recorder.min';
@@ -57,50 +58,52 @@ import isSendShortcutPressed from '../../helpers/dom/isSendShortcutPressed';
 import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
 import { MarkdownType, markdownTags } from '../../helpers/dom/getRichElementValue';
 import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret';
-import searchIndexManager from '../../lib/searchIndexManager';
+import cleanSearchText from '../../helpers/cleanSearchText';
+import EmojiHelper from './emojiHelper';
 
 const RECORD_MIN_TIME = 500;
 const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
 
 type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
 
+let selId = 0;
+
 export default class ChatInput {
-  public static AUTO_COMPLETE_REG_EXP = /(\s|^)(:|@|\/)([\S]*)$/;
-  public pageEl = document.getElementById('page-chats') as HTMLDivElement;
+  public static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*:).*|(?:(?:@|\/)(?:[\S]*)))$/;
   public messageInput: HTMLElement;
   public messageInputField: InputField;
-  public fileInput: HTMLInputElement;
-  public inputMessageContainer: HTMLDivElement;
-  public btnSend = document.getElementById('btn-send') as HTMLButtonElement;
-  public btnCancelRecord: HTMLButtonElement;
-  public lastUrl = '';
-  public lastTimeType = 0;
+  private fileInput: HTMLInputElement;
+  private inputMessageContainer: HTMLDivElement;
+  private btnSend: HTMLButtonElement;
+  private btnCancelRecord: HTMLButtonElement;
+  private lastUrl = '';
+  private lastTimeType = 0;
 
   public chatInput: HTMLElement;
-  public inputContainer: HTMLElement;
+  private inputContainer: HTMLElement;
   public rowsWrapper: HTMLDivElement;
   private newMessageWrapper: HTMLDivElement;
   private btnToggleEmoticons: HTMLButtonElement;
-  public btnSendContainer: HTMLDivElement;
+  private btnSendContainer: HTMLDivElement;
 
-  public attachMenu: HTMLButtonElement;
+  private attachMenu: HTMLButtonElement;
   private attachMenuButtons: (ButtonMenuItemOptions & {verify: (peerId: number) => boolean})[];
 
-  public sendMenu: SendMenu;
+  private sendMenu: SendMenu;
 
-  public replyElements: {
+  private replyElements: {
     container?: HTMLElement,
     cancelBtn?: HTMLButtonElement,
     titleEl?: HTMLElement,
     subtitleEl?: HTMLElement
   } = {};
 
-  public willSendWebPage: any = null;
-  public forwardingMids: number[] = [];
-  public forwardingFromPeerId: number = 0;
+  private willSendWebPage: any = null;
+  private forwardingMids: number[] = [];
+  private forwardingFromPeerId: number = 0;
   public replyToMsgId: number;
   public editMsgId: number;
-  public noWebPage: true;
+  private noWebPage: true;
   public scheduleDate: number;
   public sendSilent: true;
 
@@ -127,23 +130,35 @@ export default class ChatInput {
   readonly executedHistory: string[] = [];
   private canUndoFromHTML = '';
 
-  public stickersHelper: StickersHelper;
-  public listenerSetter: ListenerSetter;
+  private emojiHelper: EmojiHelper;
+  private stickersHelper: StickersHelper;
+  private listenerSetter: ListenerSetter;
 
-  public pinnedControlBtn: HTMLButtonElement;
+  private pinnedControlBtn: HTMLButtonElement;
 
-  public goDownBtn: HTMLButtonElement;
-  public goDownUnreadBadge: HTMLElement;
-  public btnScheduled: HTMLButtonElement;
+  private goDownBtn: HTMLButtonElement;
+  private goDownUnreadBadge: HTMLElement;
+  private btnScheduled: HTMLButtonElement;
 
-  public saveDraftDebounced: () => void;
+  private saveDraftDebounced: () => void;
 
-  public fakeRowsWrapper: HTMLDivElement;
+  private fakeRowsWrapper: HTMLDivElement;
   private fakePinnedControlBtn: HTMLElement;
 
-  public previousQuery: string;
+  private previousQuery: string;
 
-  constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager, private appDraftsManager: AppDraftsManager, private serverTimeManager: ServerTimeManager, private appNotificationsManager: AppNotificationsManager) {
+  constructor(private chat: Chat, 
+    private appMessagesManager: AppMessagesManager, 
+    private appDocsManager: AppDocsManager, 
+    private appChatsManager: AppChatsManager, 
+    private appPeersManager: AppPeersManager, 
+    private appWebPagesManager: AppWebPagesManager, 
+    private appImManager: AppImManager, 
+    private appDraftsManager: AppDraftsManager, 
+    private serverTimeManager: ServerTimeManager, 
+    private appNotificationsManager: AppNotificationsManager,
+    private appEmojiManager: AppEmojiManager
+  ) {
     this.listenerSetter = new ListenerSetter();
   }
 
@@ -348,6 +363,7 @@ export default class ChatInput {
     this.newMessageWrapper.append(...[this.btnToggleEmoticons, this.inputMessageContainer, this.btnScheduled, this.attachMenu, this.recordTimeEl, this.fileInput].filter(Boolean));
 
     this.rowsWrapper.append(this.replyElements.container);
+    this.emojiHelper = new EmojiHelper(this.rowsWrapper, this);
     this.stickersHelper = new StickersHelper(this.rowsWrapper);
     this.rowsWrapper.append(this.newMessageWrapper);
 
@@ -818,6 +834,9 @@ export default class ChatInput {
       }
     }); */
     this.listenerSetter.add(this.messageInput, 'input', this.onMessageInput);
+    this.listenerSetter.add(this.messageInput, 'keyup', () => {
+      this.checkAutocomplete();
+    });
 
     if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
       this.listenerSetter.add(this.messageInput, 'focusin', () => {
@@ -1029,10 +1048,10 @@ export default class ChatInput {
     const {value: richValue, entities: markdownEntities, caretPos} = getRichValueWithCaret(this.messageInputField.input);
       
     //const entities = RichTextProcessor.parseEntities(value);
-    const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities);
+    const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities, true);
     const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value));
 
-    this.chat.log('messageInput entities', richValue, value, markdownEntities, caretPos);
+    //this.chat.log('messageInput entities', richValue, value, markdownEntities, caretPos);
 
     if(this.stickersHelper && 
       rootScope.settings.stickers.suggest && 
@@ -1119,99 +1138,132 @@ export default class ChatInput {
       this.saveDraftDebounced();
     }
 
+    this.checkAutocomplete(richValue, caretPos);
+
     this.updateSendBtn();
   };
 
-  private checkAutocomplete(value: string, markdownEntities: MessageEntity[], entities: MessageEntity[]) {
-    const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP);
-    if(matches) {
-      if(this.previousQuery == matches[0]) {
-        return
-      }
-      this.previousQuery = matches[0]
-      var query = searchIndexManager.cleanSearchText(matches[3])
+  public onEmojiSelected = (emoji: string, autocomplete: boolean) => {
+    if(autocomplete) {
+      const {value: fullValue, caretPos} = getRichValueWithCaret(this.messageInput);
+      const pos = caretPos >= 0 ? caretPos : fullValue.length;
+      const suffix = fullValue.substr(pos);
+      const prefix = fullValue.substr(0, pos);
+      const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP);
+      console.log(matches);
 
-      /* if (matches[2] == '@') { // mentions
-        if (this.mentions && this.mentions.index) {
-          if (query.length) {
-            var foundObject = SearchIndexManager.search(query, this.mentions.index)
-            var foundUsers = []
-            var user
-            for (var i = 0, length = this.mentions.users.length; i < length; i++) {
-              user = this.mentions.users[i]
-              if (foundObject[user.id]) {
-                foundUsers.push(user)
-              }
-            }
-          } else {
-            var foundUsers = this.mentions.users
-          }
-          if (foundUsers.length) {
-            this.showMentionSuggestions(foundUsers)
-          } else {
-            this.hideSuggestions()
-          }
-        } else {
-          this.hideSuggestions()
-        }
-      } else if (!matches[1] && matches[2] == '/') { // commands
-        if (this.commands && this.commands.index) {
-          if (query.length) {
-            var foundObject = SearchIndexManager.search(query, this.commands.index)
-            var foundCommands = []
-            var command
-            for (var i = 0, length = this.commands.list.length; i < length; i++) {
-              command = this.commands.list[i]
-              if (foundObject[command.value]) {
-                foundCommands.push(command)
-              }
-            }
-          } else {
-            var foundCommands = this.commands.list
-          }
-          if (foundCommands.length) {
-            this.showCommandsSuggestions(foundCommands)
-          } else {
-            this.hideSuggestions()
-          }
-        } else {
-          this.hideSuggestions()
-        }
-      } else  *//* if(matches[2] === ':') { // emoji
-        if(value.match(/^\s*:(.+):\s*$/)) {
-          return;
-        }
-        
-        EmojiHelper.getPopularEmoji((function (popular) {
-          if (query.length) {
-            var found = EmojiHelper.searchEmojis(query)
-            if (found.length) {
-              var popularFound = [],
-                code
-              var pos
-              for (var i = 0, len = popular.length; i < len; i++) {
-                code = popular[i].code
-                pos = found.indexOf(code)
-                if (pos >= 0) {
-                  popularFound.push(code)
-                  found.splice(pos, 1)
-                  if (!found.length) {
-                    break
-                  }
-                }
-              }
-              this.showEmojiSuggestions(popularFound.concat(found))
-            } else {
-              this.hideSuggestions()
-            }
-          } else {
-            this.showEmojiSuggestions(popular)
-          }
-        }).bind(this))
+      const idx = matches.index + matches[1].length;
+
+      //const str = 
+
+      /* var newValuePrefix
+      if(matches && matches[0]) {
+        newValuePrefix = prefix.substr(0, matches.index) + ':' + emoji[1] + ':'
+      } else {
+        newValuePrefix = prefix + ':' + emoji[1] + ':'
       }
-    } else {
-      delete this.previousQuery
-      this.hideSuggestions() */
+
+      if(suffix.length) {
+        const html = this.getRichHtml(newValuePrefix) + '&nbsp;<span id="composer_sel' + ++selId + '"></span>' + this.getRichHtml(suffix)
+        this.richTextareaEl.html(html)
+        setRichFocus(textarea, $('#composer_sel' + this.selId)[0])
+      } else {
+        const html = this.getRichHtml(newValuePrefix) + '&nbsp;'
+        this.richTextareaEl.html(html)
+        setRichFocus(textarea)
+      } */
+    }
+  };
+
+  private checkAutocomplete(value?: string, caretPos?: number) {
+    return;
+    
+    if(value === undefined) {
+      const r = getRichValueWithCaret(this.messageInputField.input, false);
+      value = r.value;
+      caretPos = r.caretPos;
+    }
+
+    if(caretPos === -1) {
+      caretPos = value.length;
+    }
+    value = value.substr(0, caretPos);
+
+    const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP);
+    if(!matches) {
+      delete this.previousQuery;
+      //this.hideSuggestions();
+      this.emojiHelper.toggle(true);
+      return;
+    }
+
+    if(this.previousQuery === matches[0]) {
+      return;
+    }
+    
+    this.previousQuery = matches[0];
+    //let query = cleanSearchText(matches[2]);
+    //const firstChar = matches[2][0];
+
+    //console.log('autocomplete matches', matches);
+
+    /*if (matches[2] == '@') { // mentions
+      if (this.mentions && this.mentions.index) {
+        if (query.length) {
+          var foundObject = SearchIndexManager.search(query, this.mentions.index)
+          var foundUsers = []
+          var user
+          for (var i = 0, length = this.mentions.users.length; i < length; i++) {
+            user = this.mentions.users[i]
+            if (foundObject[user.id]) {
+              foundUsers.push(user)
+            }
+          }
+        } else {
+          var foundUsers = this.mentions.users
+        }
+        if (foundUsers.length) {
+          this.showMentionSuggestions(foundUsers)
+        } else {
+          this.hideSuggestions()
+        }
+      } else {
+        this.hideSuggestions()
+      }
+    } else if (!matches[1] && matches[2] == '/') { // commands
+      if (this.commands && this.commands.index) {
+        if (query.length) {
+          var foundObject = SearchIndexManager.search(query, this.commands.index)
+          var foundCommands = []
+          var command
+          for (var i = 0, length = this.commands.list.length; i < length; i++) {
+            command = this.commands.list[i]
+            if (foundObject[command.value]) {
+              foundCommands.push(command)
+            }
+          }
+        } else {
+          var foundCommands = this.commands.list
+        }
+        if (foundCommands.length) {
+          this.showCommandsSuggestions(foundCommands)
+        } else {
+          this.hideSuggestions()
+        }
+      } else {
+        this.hideSuggestions()
+      }
+    } else  *//* if(firstChar === ':') */ { // emoji
+      if(value.match(/^\s*:(.+):\s*$/)) {
+        this.emojiHelper.toggle(true);
+        return;
+      }
+      
+      this.appEmojiManager.getBothEmojiKeywords().then(() => {
+        const emojis = this.appEmojiManager.searchEmojis(matches[2]);
+        this.emojiHelper.renderEmojis(emojis);
+        //console.log(emojis);
+      });
     }
   }
 
diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts
index e7498482..030b3144 100644
--- a/src/components/emoticonsDropdown/tabs/emoji.ts
+++ b/src/components/emoticonsDropdown/tabs/emoji.ts
@@ -17,8 +17,92 @@ import { putPreloader } from "../../misc";
 import Scrollable from "../../scrollable";
 import StickyIntersector from "../../stickyIntersector";
 
+const loadedURLs: Set<string> = new Set();
+export function appendEmoji(emoji: string, container: HTMLElement, prepend = false/* , unified = false */) {
+  //const emoji = details.unified;
+  //const emoji = (details.unified as string).split('-')
+  //.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
+
+  const spanEmoji = document.createElement('span');
+  spanEmoji.classList.add('super-emoji');
+
+  let kek: string;
+  /* if(unified) {
+    kek = RichTextProcessor.wrapRichText('_', {
+      entities: [{
+        _: 'messageEntityEmoji',
+        offset: 0,
+        length: emoji.split('-').length,
+        unicode: emoji
+      }]
+    });
+  } else { */
+    kek = RichTextProcessor.wrapEmojiText(emoji);
+  //}
+
+  /* if(!kek.includes('emoji')) {
+    console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
+    return;
+  } */
+
+  //console.log(kek);
+
+  spanEmoji.innerHTML = kek;
+
+  if(spanEmoji.children.length > 1) {
+    const first = spanEmoji.firstElementChild;
+    spanEmoji.innerHTML = '';
+    spanEmoji.append(first);
+  }
+
+  if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) {
+    const image = spanEmoji.firstElementChild as HTMLImageElement;
+    image.setAttribute('loading', 'lazy');
+
+    const url = image.src;
+    if(!loadedURLs.has(url)) {
+      const placeholder = document.createElement('span');
+      placeholder.classList.add('emoji-placeholder');
+
+      if(rootScope.settings.animationsEnabled) {
+        image.style.opacity = '0';
+        placeholder.style.opacity = '1';
+      }
+
+      image.addEventListener('load', () => {
+        fastRaf(() => {
+          if(rootScope.settings.animationsEnabled) {
+            image.style.opacity = '';
+            placeholder.style.opacity = '';
+          }
+
+          spanEmoji.classList.remove('empty');
+
+          loadedURLs.add(url);
+        });
+      }, {once: true});
+
+      spanEmoji.append(placeholder);
+    }
+  }
+
+  //spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
+  //spanEmoji.setAttribute('emoji', emoji);
+  if(prepend) container.prepend(spanEmoji);
+  else container.appendChild(spanEmoji);
+}
+
+export function getEmojiFromElement(element: HTMLElement) {
+  if(element.nodeType === 3) return element.nodeValue;
+  if(element.tagName === 'SPAN' && !element.classList.contains('emoji')) {
+    element = element.firstElementChild as HTMLElement;
+  }
+  
+  return element.getAttribute('alt') || element.innerText;
+}
+
 export default class EmojiTab implements EmoticonsTab {
-  public content: HTMLElement;
+  private content: HTMLElement;
 
   private recent: string[] = [];
   private recentItemsDiv: HTMLElement;
@@ -26,8 +110,6 @@ export default class EmojiTab implements EmoticonsTab {
   private scroll: Scrollable;
   private stickyIntersector: StickyIntersector;
 
-  private loadedURLs: Set<string> = new Set();
-
   init() {
     this.content = document.getElementById('content-emoji') as HTMLDivElement;
 
@@ -84,7 +166,7 @@ export default class EmojiTab implements EmoticonsTab {
       titleDiv.append(i18n(category));
 
       const itemsDiv = document.createElement('div');
-      itemsDiv.classList.add('category-items');
+      itemsDiv.classList.add('super-emojis');
 
       div.append(titleDiv, itemsDiv);
 
@@ -95,7 +177,7 @@ export default class EmojiTab implements EmoticonsTab {
 
         emoji = emoji.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
 
-        this.appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */);
+        appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */);
 
         /* if(category === 'Smileys & Emotion') {
           console.log('appended emoji', emoji, itemsDiv.children[itemsDiv.childElementCount - 1].innerHTML, emojiUnicode(emoji));
@@ -125,9 +207,9 @@ export default class EmojiTab implements EmoticonsTab {
     ]).then(() => {
       preloader.remove();
 
-      this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.category-items');
+      this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.super-emojis');
       for(const emoji of this.recent) {
-        this.appendEmoji(emoji, this.recentItemsDiv);
+        appendEmoji(emoji, this.recentItemsDiv);
       }
 
       this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length);
@@ -151,95 +233,12 @@ export default class EmojiTab implements EmoticonsTab {
     this.init = null;
   }
 
-  private appendEmoji(emoji: string, container: HTMLElement, prepend = false/* , unified = false */) {
-    //const emoji = details.unified;
-    //const emoji = (details.unified as string).split('-')
-    //.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
-
-    const spanEmoji = document.createElement('span');
-    spanEmoji.classList.add('category-item');
-
-    let kek: string;
-    /* if(unified) {
-      kek = RichTextProcessor.wrapRichText('_', {
-        entities: [{
-          _: 'messageEntityEmoji',
-          offset: 0,
-          length: emoji.split('-').length,
-          unicode: emoji
-        }]
-      });
-    } else { */
-      kek = RichTextProcessor.wrapEmojiText(emoji);
-    //}
-
-    /* if(!kek.includes('emoji')) {
-      console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
-      return;
-    } */
-  
-    //console.log(kek);
-  
-    spanEmoji.innerHTML = kek;
-
-    if(spanEmoji.children.length > 1) {
-      const first = spanEmoji.firstElementChild;
-      spanEmoji.innerHTML = '';
-      spanEmoji.append(first);
-    }
-
-    if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) {
-      const image = spanEmoji.firstElementChild as HTMLImageElement;
-      image.setAttribute('loading', 'lazy');
-
-      const url = image.src;
-      if(!this.loadedURLs.has(url)) {
-        const placeholder = document.createElement('span');
-        placeholder.classList.add('emoji-placeholder');
-  
-        if(rootScope.settings.animationsEnabled) {
-          image.style.opacity = '0';
-          placeholder.style.opacity = '1';
-        }
-  
-        image.addEventListener('load', () => {
-          fastRaf(() => {
-            if(rootScope.settings.animationsEnabled) {
-              image.style.opacity = '';
-              placeholder.style.opacity = '';
-            }
-  
-            spanEmoji.classList.remove('empty');
-  
-            this.loadedURLs.add(url);
-          });
-        }, {once: true});
-  
-        spanEmoji.append(placeholder);
-      }
-    }
-  
-    //spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
-    //spanEmoji.setAttribute('emoji', emoji);
-    if(prepend) container.prepend(spanEmoji);
-    else container.appendChild(spanEmoji);
-  }
-
-  private getEmojiFromElement(element: HTMLElement) {
-    if(element.nodeType === 3) return element.nodeValue;
-    if(element.tagName === 'SPAN' && !element.classList.contains('emoji')) {
-      element = element.firstElementChild as HTMLElement;
-    }
-    
-    return element.getAttribute('alt') || element.innerText;
-  }
-
   onContentClick = (e: MouseEvent) => {
     let target = e.target as HTMLElement;
     //if(target.tagName !== 'SPAN') return;
 
     if(target.tagName === 'SPAN' && !target.classList.contains('emoji')) {
-      target = findUpClassName(target, 'category-item');
+      target = findUpClassName(target, 'super-emoji');
       if(!target) {
         return;
       }
@@ -253,15 +252,15 @@ export default class EmojiTab implements EmoticonsTab {
       target.outerHTML;
 
     // Recent
-    const emoji = this.getEmojiFromElement(target);
+    const emoji = getEmojiFromElement(target);
     (Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => {
-      const _emoji = this.getEmojiFromElement(el);
+      const _emoji = getEmojiFromElement(el);
       if(emoji === _emoji) {
         el.remove();
       }
     });
-    const scrollHeight = this.recentItemsDiv.scrollHeight;
-    this.appendEmoji(emoji, this.recentItemsDiv, true);
+    //const scrollHeight = this.recentItemsDiv.scrollHeight;
+    appendEmoji(emoji, this.recentItemsDiv, true);
 
     this.recent.findAndSplice(e => e === emoji);
     this.recent.unshift(emoji);
diff --git a/src/components/emoticonsDropdown/tabs/gifs.ts b/src/components/emoticonsDropdown/tabs/gifs.ts
index 3766f751..081bdb96 100644
--- a/src/components/emoticonsDropdown/tabs/gifs.ts
+++ b/src/components/emoticonsDropdown/tabs/gifs.ts
@@ -12,7 +12,7 @@ import apiManager from "../../../lib/mtproto/mtprotoworker";
 import appDocsManager, {MyDocument} from "../../../lib/appManagers/appDocsManager";
 
 export default class GifsTab implements EmoticonsTab {
-  public content: HTMLElement;
+  private content: HTMLElement;
 
   init() {
     this.content = document.getElementById('content-gifs');
@@ -45,4 +45,4 @@ export default class GifsTab implements EmoticonsTab {
   onClose() {
 
   }
-}
\ No newline at end of file
+}
diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts
index 199ee08d..142dfd56 100644
--- a/src/components/emoticonsDropdown/tabs/stickers.ts
+++ b/src/components/emoticonsDropdown/tabs/stickers.ts
@@ -24,8 +24,8 @@ import StickyIntersector from "../../stickyIntersector";
 import { wrapSticker } from "../../wrappers";
 
 export class SuperStickerRenderer {
-  lazyLoadQueue: LazyLoadQueueRepeat;
-  animatedDivs: Set<HTMLDivElement> = new Set();
+  public lazyLoadQueue: LazyLoadQueueRepeat;
+  private animatedDivs: Set<HTMLDivElement> = new Set();
 
   constructor(private regularLazyLoadQueue: LazyLoadQueue, private group: string) {
     this.lazyLoadQueue = new LazyLoadQueueRepeat(undefined, (target, visible) => {
@@ -35,7 +35,7 @@ export class SuperStickerRenderer {
     });
   }
 
-  renderSticker(doc: MyDocument, div?: HTMLDivElement, loadPromises?: Promise<any>[]) {
+  public renderSticker(doc: MyDocument, div?: HTMLDivElement, loadPromises?: Promise<any>[]) {
     if(!div) {
       div = document.createElement('div');
       div.classList.add('grid-item', 'super-sticker');
@@ -63,7 +63,7 @@ export class SuperStickerRenderer {
     return div;
   }
 
-  checkAnimationContainer = (div: HTMLElement, visible: boolean) => {
+  private checkAnimationContainer = (div: HTMLElement, visible: boolean) => {
     //console.error('checkAnimationContainer', div, visible);
     const players = animationIntersector.getAnimations(div);
     players.forEach(player => {
@@ -75,7 +75,7 @@ export class SuperStickerRenderer {
     });
   };
 
-  processVisibleDiv = (div: HTMLElement) => {
+  private processVisibleDiv = (div: HTMLElement) => {
     const docId = div.dataset.docId;
     const doc = appDocsManager.getDoc(docId);
     
@@ -107,7 +107,7 @@ export class SuperStickerRenderer {
     return promise;
   };
 
-  processInvisibleDiv = (div: HTMLElement) => {
+  public processInvisibleDiv = (div: HTMLElement) => {
     const docId = div.dataset.docId;
     const doc = appDocsManager.getDoc(docId);
 
@@ -121,7 +121,7 @@ export class SuperStickerRenderer {
 }
 
 export default class StickersTab implements EmoticonsTab {
-  public content: HTMLElement;
+  private content: HTMLElement;
   private stickersDiv: HTMLElement;
 
   private stickerSets: {[id: string]: {
@@ -381,4 +381,4 @@ export default class StickersTab implements EmoticonsTab {
   onClose() {
 
   }
-}
\ No newline at end of file
+}
diff --git a/src/helpers/cleanSearchText.ts b/src/helpers/cleanSearchText.ts
new file mode 100644
index 00000000..cd8d6818
--- /dev/null
+++ b/src/helpers/cleanSearchText.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 Config from "../lib/config";
+
+const badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<]+/g;
+const trimRe = /^\s+|\s$/g;
+
+export default function cleanSearchText(text: string, latinize = true) {
+  const hasTag = text.charAt(0) === '%';
+  text = text.replace(badCharsRe, '').replace(trimRe, '');
+  if(latinize) {
+    text = text.replace(/[^A-Za-z0-9]/g, (ch) => {
+      const latinizeCh = Config.LatinizeMap[ch];
+      return latinizeCh !== undefined ? latinizeCh : ch;
+    });
+  }
+  
+  text = text.toLowerCase();
+  if(hasTag) {
+    text = '%' + text;
+  }
+
+  return text;
+}
diff --git a/src/helpers/cleanUsername.ts b/src/helpers/cleanUsername.ts
new file mode 100644
index 00000000..1c531c69
--- /dev/null
+++ b/src/helpers/cleanUsername.ts
@@ -0,0 +1,14 @@
+/*
+ * 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
+ */
+
+export default function cleanUsername(username: string) {
+  return username && username.toLowerCase() || '';
+}
diff --git a/src/helpers/dom/attachListNavigation.ts b/src/helpers/dom/attachListNavigation.ts
index 2139d586..2c613fc0 100644
--- a/src/helpers/dom/attachListNavigation.ts
+++ b/src/helpers/dom/attachListNavigation.ts
@@ -27,7 +27,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
     return target || list.querySelector('.' + ACTIVE_CLASS_NAME) || list.firstElementChild;
   };
 
-  const setCurrentTarget = (_target: Element) => {
+  const setCurrentTarget = (_target: Element, scrollTo: boolean) => {
     if(target === _target) {
       return;
     }
@@ -41,7 +41,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
     target = _target;
     target.classList.add(ACTIVE_CLASS_NAME);
 
-    if(hadTarget && scrollable) {
+    if(hadTarget && scrollable && scrollTo) {
       fastSmoothScroll(scrollable, target as HTMLElement, 'center', undefined, undefined, undefined, 100, type === 'x' ? 'x' : 'y');
     }
   };
@@ -97,7 +97,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
     if(list.childElementCount > 1) {
       let currentTarget = getCurrentTarget();
       currentTarget = handleArrowKey(currentTarget, e.key as any);
-      setCurrentTarget(currentTarget);
+      setCurrentTarget(currentTarget, true);
     }
 
     return false;
@@ -112,7 +112,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
       return;
     }
 
-    setCurrentTarget(target);
+    setCurrentTarget(target, false);
   };
 
   const onClick = (e: Event) => {
@@ -123,7 +123,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
       return;
     }
 
-    setCurrentTarget(target);
+    setCurrentTarget(target, false);
     fireSelect(getCurrentTarget());
   };
 
@@ -142,7 +142,7 @@ export default function attachListNavigation({list, type, onSelect, once}: {
   };
 
   const resetTarget = () => {
-    setCurrentTarget(list.firstElementChild);
+    setCurrentTarget(list.firstElementChild, false);
   };
 
   resetTarget();
diff --git a/src/helpers/dom/renderImageFromUrl.ts b/src/helpers/dom/renderImageFromUrl.ts
index 488e1def..8d840956 100644
--- a/src/helpers/dom/renderImageFromUrl.ts
+++ b/src/helpers/dom/renderImageFromUrl.ts
@@ -15,7 +15,7 @@ const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoE
 export default function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string, callback?: (err?: Event) => void, useCache = true): boolean {
   if(!url) {
     console.error('renderImageFromUrl: no url?', elem, url);
-    //callback && callback();
+    callback && callback();
     return false;
   }
 
diff --git a/src/lib/appManagers/appEmojiManager.ts b/src/lib/appManagers/appEmojiManager.ts
new file mode 100644
index 00000000..64c9430b
--- /dev/null
+++ b/src/lib/appManagers/appEmojiManager.ts
@@ -0,0 +1,185 @@
+import App from "../../config/app";
+import { MOUNT_CLASS_TO } from "../../config/debug";
+import { validateInitObject } from "../../helpers/object";
+import I18n from "../langPack";
+import { isObject } from "../mtproto/bin_utils";
+import apiManager from "../mtproto/mtprotoworker";
+import SearchIndex from "../searchIndex";
+import sessionStorage from "../sessionStorage";
+
+type EmojiLangPack = {
+  keywords: {
+    [keyword: string]: string[],
+  },
+  version: number,
+  langCode: string
+};
+
+const EMOJI_LANG_PACK: EmojiLangPack = {
+  keywords: {},
+  version: 0,
+  langCode: App.langPackCode
+};
+
+export class AppEmojiManager {
+  private keywordLangPacks: {
+    [langCode: string]: EmojiLangPack
+  } = {};
+
+  private index: SearchIndex<EmojiLangPack['keywords'][keyof EmojiLangPack['keywords']]>;
+  private indexedLangPacks: {[langCode: string]: boolean} = {};
+
+  private getKeywordsPromises: {[langCode: string]: Promise<EmojiLangPack>} = {};
+
+  /* public getPopularEmoji() {
+    return sessionStorage.get('emojis_popular').then(popEmojis => {
+      var result = []
+      if (popEmojis && popEmojis.length) {
+        for (var i = 0, len = popEmojis.length; i < len; i++) {
+          result.push({code: popEmojis[i][0], rate: popEmojis[i][1]})
+        }
+        callback(result)
+        return
+      }
+
+      return sessionStorage.get('emojis_recent').then(recentEmojis => {
+        recentEmojis = recentEmojis || popular || []
+        var shortcut
+        var code
+        for (var i = 0, len = recentEmojis.length; i < len; i++) {
+          shortcut = recentEmojis[i]
+          if (Array.isArray(shortcut)) {
+            shortcut = shortcut[0]
+          }
+          if (shortcut && typeof shortcut === 'string') {
+            if (shortcut.charAt(0) == ':') {
+              shortcut = shortcut.substr(1, shortcut.length - 2)
+            }
+            if (code = shortcuts[shortcut]) {
+              result.push({code: code, rate: 1})
+            }
+          }
+        }
+        callback(result)
+      });
+    });
+  }
+
+  function pushPopularEmoji (code) {
+    getPopularEmoji(function (popularEmoji) {
+      var exists = false
+      var count = popularEmoji.length
+      var result = []
+      for (var i = 0; i < count; i++) {
+        if (popularEmoji[i].code == code) {
+          exists = true
+          popularEmoji[i].rate++
+        }
+        result.push([popularEmoji[i].code, popularEmoji[i].rate])
+      }
+      if (exists) {
+        result.sort(function (a, b) {
+          return b[1] - a[1]
+        })
+      } else {
+        if (result.length > 41) {
+          result = result.slice(0, 41)
+        }
+        result.push([code, 1])
+      }
+      ConfigStorage.set({emojis_popular: result})
+    })
+  } */
+
+  public getEmojiKeywords(langCode: string = App.langPackCode) {
+    const promise = this.getKeywordsPromises[langCode];
+    if(promise) {
+      return promise;
+    }
+
+    const storageKey: any = 'emojiKeywords_' + langCode;
+    return this.getKeywordsPromises[langCode] = sessionStorage.get(storageKey).then((pack: EmojiLangPack) => {
+      if(!isObject(pack)) {
+        pack = {} as any;
+      }
+
+      validateInitObject(EMOJI_LANG_PACK, pack);
+
+      // important
+      pack.langCode = langCode;
+      this.keywordLangPacks[langCode] = pack;
+
+      return apiManager.invokeApi('messages.getEmojiKeywordsDifference', {
+        lang_code: pack.langCode,
+        from_version: pack.version
+      }).then((keywordsDifference) => {
+        pack.version = keywordsDifference.version;
+        
+        const packKeywords = pack.keywords;
+        const keywords = keywordsDifference.keywords;
+        for(let i = 0, length = keywords.length; i < length; ++i) {
+          const {keyword, emoticons} = keywords[i];
+          packKeywords[keyword] = emoticons;
+        }
+
+        sessionStorage.set({
+          [storageKey]: pack
+        });
+
+        return pack;
+      }, () => {
+        return pack;
+      });
+    });
+  }
+
+  public getBothEmojiKeywords() {
+    const promises: ReturnType<AppEmojiManager['getEmojiKeywords']>[] = [
+      this.getEmojiKeywords()
+    ];
+
+    if(I18n.lastRequestedLangCode !== App.langPackCode) {
+      promises.push(this.getEmojiKeywords(I18n.lastRequestedLangCode));
+    }
+
+    return Promise.all(promises);
+  }
+
+  public indexEmojis() {
+    if(!this.index) {
+      this.index = new SearchIndex();
+    }
+
+    for(const langCode in this.keywordLangPacks) {
+      if(this.indexedLangPacks[langCode]) {
+        continue;
+      }
+
+      const pack = this.keywordLangPacks[langCode];
+      const keywords = pack.keywords;
+
+      for(const keyword in keywords) {
+        const emoticons = keywords[keyword];
+        this.index.indexObject(emoticons, keyword);
+      }
+
+      this.indexedLangPacks[langCode] = true;
+    }
+  }
+
+  public searchEmojis(q: string) {
+    this.indexEmojis();
+    
+    //const perf = performance.now();
+    const set = this.index.search(q);
+    const flattened = Array.from(set).reduce((acc, v) => acc.concat(v), []);
+    const emojis = Array.from(new Set(flattened));
+    //console.log('searchEmojis', q, 'time', performance.now() - perf);
+
+    return emojis;
+  }
+}
+
+const appEmojiManager = new AppEmojiManager();
+MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appEmojiManager = appEmojiManager);
+export default appEmojiManager;
diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts
index 818f472d..1c6dd525 100644
--- a/src/lib/appManagers/appImManager.ts
+++ b/src/lib/appManagers/appImManager.ts
@@ -56,6 +56,7 @@ import disableTransition from '../../helpers/dom/disableTransition';
 import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
 import replaceContent from '../../helpers/dom/replaceContent';
 import whichChild from '../../helpers/dom/whichChild';
+import appEmojiManager from './appEmojiManager';
 
 //console.log('appImManager included33!');
 
@@ -726,7 +727,25 @@ export class AppImManager {
   }
 
   private createNewChat() {
-    const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appPollsManager, apiManager, appDraftsManager, serverTimeManager, sessionStorage, appNotificationsManager);
+    const chat = new Chat(this, 
+      appChatsManager, 
+      appDocsManager, 
+      appInlineBotsManager, 
+      appMessagesManager, 
+      appPeersManager, 
+      appPhotosManager, 
+      appProfileManager, 
+      appStickersManager, 
+      appUsersManager, 
+      appWebPagesManager, 
+      appPollsManager, 
+      apiManager, 
+      appDraftsManager, 
+      serverTimeManager, 
+      sessionStorage, 
+      appNotificationsManager, 
+      appEmojiManager
+    );
 
     if(this.chats.length) {
       chat.backgroundEl.append(this.chat.backgroundEl.lastElementChild.cloneNode(true));
diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts
index f3d17a5d..e4f46b44 100644
--- a/src/lib/appManagers/appUsersManager.ts
+++ b/src/lib/appManagers/appUsersManager.ts
@@ -11,6 +11,8 @@
 
 import { formatPhoneNumber } from "../../components/misc";
 import { MOUNT_CLASS_TO } from "../../config/debug";
+import cleanSearchText from "../../helpers/cleanSearchText";
+import cleanUsername from "../../helpers/cleanUsername";
 import { tsNow } from "../../helpers/date";
 import { safeReplaceObject, isObject } from "../../helpers/object";
 import { InputUser, Update, User as MTUser, UserStatus } from "../../layer";
@@ -21,7 +23,7 @@ import { REPLIES_PEER_ID } from "../mtproto/mtproto_config";
 import serverTimeManager from "../mtproto/serverTimeManager";
 import { RichTextProcessor } from "../richtextprocessor";
 import rootScope from "../rootScope";
-import searchIndexManager from "../searchIndexManager";
+import SearchIndex from "../searchIndex";
 import apiUpdatesManager from "./apiUpdatesManager";
 import appChatsManager from "./appChatsManager";
 import appPeersManager from "./appPeersManager";
@@ -36,7 +38,7 @@ export class AppUsersManager {
   
   private users: {[userId: number]: User} = {};
   private usernames: {[username: string]: number} = {};
-  private contactsIndex = searchIndexManager.createIndex();
+  private contactsIndex = new SearchIndex<number>();
   private contactsFillPromise: Promise<Set<number>>;
   private contactsList: Set<number> = new Set();
   private updatedContactsList = false;
@@ -110,7 +112,7 @@ export class AppUsersManager {
 
     rootScope.on('language_change', (e) => {
       const userId = this.getSelf().id;
-      searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex);
+      this.contactsIndex.indexObject(userId, this.getUserSearchText(userId));
     });
 
     appStateManager.getState().then((state) => {
@@ -207,7 +209,7 @@ export class AppUsersManager {
 
   public pushContact(userId: number) {
     this.contactsList.add(userId);
-    searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex);
+    this.contactsIndex.indexObject(userId, this.getUserSearchText(userId));
     appStateManager.requestPeer(userId, 'contacts');
   }
 
@@ -233,8 +235,8 @@ export class AppUsersManager {
     return this.fillContacts().then(_contactsList => {
       let contactsList = [..._contactsList];
       if(query) {
-        const results = searchIndexManager.search(query, this.contactsIndex);
-        const filteredContactsList = [...contactsList].filter(id => !!results[id]);
+        const results = this.contactsIndex.search(query);
+        const filteredContactsList = [...contactsList].filter(id => results.has(id));
 
         contactsList = filteredContactsList;
       }
@@ -288,9 +290,9 @@ export class AppUsersManager {
 
   public testSelfSearch(query: string) {
     const user = this.getSelf();
-    const index = searchIndexManager.createIndex();
-    searchIndexManager.indexObject(user.id, this.getUserSearchText(user.id), index);
-    return !!searchIndexManager.search(query, index)[user.id];
+    const index = new SearchIndex();
+    index.indexObject(user.id, this.getUserSearchText(user.id));
+    return index.search(query).has(user.id);
   }
 
   public saveApiUsers(apiUsers: any[], override?: boolean) {
@@ -320,11 +322,11 @@ export class AppUsersManager {
 
     const fullName = user.first_name + ' ' + (user.last_name || '');
     if(user.username) {
-      const searchUsername = searchIndexManager.cleanUsername(user.username);
+      const searchUsername = cleanUsername(user.username);
       this.usernames[searchUsername] = userId;
     }
 
-    user.sortName = user.pFlags.deleted ? '' : searchIndexManager.cleanSearchText(fullName, false);
+    user.sortName = user.pFlags.deleted ? '' : cleanSearchText(fullName, false);
 
     user.initials = RichTextProcessor.getAbbreviation(fullName);
 
diff --git a/src/lib/mtproto/networkerFactory.ts b/src/lib/mtproto/networkerFactory.ts
index 2c4f8b24..c7c522c2 100644
--- a/src/lib/mtproto/networkerFactory.ts
+++ b/src/lib/mtproto/networkerFactory.ts
@@ -16,6 +16,7 @@ import MTTransport from "./transports/transport";
 export class NetworkerFactory {
   public updatesProcessor: (obj: any) => void = null;
   public onConnectionStatusChange: (info: ConnectionStatusChange) => void = null;
+  public akStopped = false;
 
   public setUpdatesProcessor(callback: (obj: any) => void) {
     this.updatesProcessor = callback;
@@ -25,6 +26,17 @@ export class NetworkerFactory {
     //console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options);
     return new MTPNetworker(dcId, authKey, authKeyID, serverSalt, transport, options);
   }
+
+  public startAll() {
+    if(this.akStopped) {
+      this.akStopped = false;
+      this.updatesProcessor && this.updatesProcessor({_: 'new_session_created'});
+    }
+  }
+
+  public stopAll() {
+    this.akStopped = true;
+  }
 }
 
 export default new NetworkerFactory();
diff --git a/src/lib/mtproto/singleInstance.ts b/src/lib/mtproto/singleInstance.ts
new file mode 100644
index 00000000..08ce5210
--- /dev/null
+++ b/src/lib/mtproto/singleInstance.ts
@@ -0,0 +1,113 @@
+import { MOUNT_CLASS_TO } from "../../config/debug";
+import { nextRandomInt } from "../../helpers/random";
+import { logger } from "../logger";
+import rootScope from "../rootScope";
+import sessionStorage from "../sessionStorage";
+
+export type AppInstance = {
+  id: number,
+  idle: boolean,
+  time: number
+};
+
+export class SingleInstance {
+  private instanceID = nextRandomInt(0xFFFFFFFF);
+  private started = false;
+  private masterInstance = false;
+  private deactivateTimeout: number = 0;
+  private deactivated = false;
+  private initial = false;
+  private log = logger('SI');
+
+  public start() {
+    if(!this.started/*  && !Config.Navigator.mobile && !Config.Modes.packed */) {
+      this.started = true
+
+      //IdleManager.start();
+
+      rootScope.addEventListener('idle', this.checkInstance);
+      setInterval(this.checkInstance, 5000);
+      this.checkInstance();
+
+      try {
+        document.documentElement.addEventListener('beforeunload', this.clearInstance);
+      } catch(e) {}
+    }
+  }
+
+  public clearInstance() {
+    if(this.masterInstance && !this.deactivated) {
+      this.log.warn('clear master instance');
+      sessionStorage.delete('xt_instance');
+    }
+  }
+
+  public deactivateInstance = () => {
+    if(this.masterInstance || this.deactivated) {
+      return false;
+    }
+
+    this.log('deactivate');
+    this.deactivateTimeout = 0;
+    this.deactivated = true;
+    this.clearInstance();
+    //$modalStack.dismissAll();
+
+    //document.title = _('inactive_tab_title_raw')
+
+    rootScope.idle.deactivated = true;
+  };
+
+  public checkInstance = () => {
+    if(this.deactivated) {
+      return false;
+    }
+    
+    const time = Date.now();
+    const idle = rootScope.idle && rootScope.idle.isIDLE;
+    const newInstance: AppInstance = {
+      id: this.instanceID, 
+      idle, 
+      time
+    };
+
+    sessionStorage.get('xt_instance').then((curInstance: AppInstance) => {
+      // console.log(dT(), 'check instance', newInstance, curInstance)
+      if(!idle ||
+          !curInstance ||
+          curInstance.id == this.instanceID ||
+          curInstance.time < time - 20000) {
+        sessionStorage.set({xt_instance: newInstance});
+        if(!this.masterInstance) {
+          //MtpNetworkerFactory.startAll();
+          if(!this.initial) {
+            this.initial = true;
+          } else {
+            this.log.warn('now master instance', newInstance);
+          }
+
+          this.masterInstance = true;
+        }
+
+        if(this.deactivateTimeout) {
+          clearTimeout(this.deactivateTimeout);
+          this.deactivateTimeout = 0;
+        }
+      } else {
+        if(this.masterInstance) {
+          //MtpNetworkerFactory.stopAll();
+          this.log.warn('now idle instance', newInstance);
+          if(!this.deactivateTimeout) {
+            this.deactivateTimeout = window.setTimeout(this.deactivateInstance, 30000);
+          }
+
+          this.masterInstance = false;
+        }
+      }
+    });
+  };
+}
+
+const singleInstance = new SingleInstance();
+MOUNT_CLASS_TO && (MOUNT_CLASS_TO.singleInstance = singleInstance);
+export default singleInstance;
diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts
index 0aa64ec9..b768bcbe 100644
--- a/src/lib/richtextprocessor.ts
+++ b/src/lib/richtextprocessor.ts
@@ -238,7 +238,7 @@ namespace RichTextProcessor {
     })
   } */
 
-  export function parseMarkdown(text: string, currentEntities: MessageEntity[], noTrim?: any): string {
+  export function parseMarkdown(text: string, currentEntities: MessageEntity[], noTrim?: boolean): string {
     /* if(!markdownTestRegExp.test(text)) {
       return noTrim ? text : text.trim();
     } */
diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts
index 81d6816f..2840054e 100644
--- a/src/lib/rootScope.ts
+++ b/src/lib/rootScope.ts
@@ -122,6 +122,7 @@ export class RootScope extends EventListenerBase<{
   public myId = 0;
   public idle = {
     isIDLE: true,
+    deactivated: false,
     focusPromise: Promise.resolve(),
     focusResolve: () => {}
   };
@@ -158,20 +159,30 @@ export class RootScope extends EventListenerBase<{
   }
 
   public setThemeListener() {
-    const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
-    const checkDarkMode = () => {
-      //const theme = this.getTheme();
-      this.systemTheme = darkModeMediaQuery.matches ? 'night' : 'day';
-      //const newTheme = this.getTheme();
+    try {
+      const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+      const checkDarkMode = () => {
+        //const theme = this.getTheme();
+        this.systemTheme = darkModeMediaQuery.matches ? 'night' : 'day';
+        //const newTheme = this.getTheme();
 
-      if(this.myId) {
-        this.broadcast('theme_change');
-      } else {
-        this.setTheme();
+        if(this.myId) {
+          this.broadcast('theme_change');
+        } else {
+          this.setTheme();
+        }
+      };
+
+      if('addEventListener' in darkModeMediaQuery) {
+        darkModeMediaQuery.addEventListener('change', checkDarkMode);
+      } else if('addListener' in darkModeMediaQuery) {
+        (darkModeMediaQuery as any).addListener(checkDarkMode);
       }
-    };
-    darkModeMediaQuery.addEventListener('change', checkDarkMode);
-    checkDarkMode();
+
+      checkDarkMode();
+    } catch(err) {
+
+    }
   }
 
   public setTheme() {
diff --git a/src/lib/searchIndexManager.ts b/src/lib/searchIndex.ts
similarity index 56%
rename from src/lib/searchIndexManager.ts
rename to src/lib/searchIndex.ts
index ee2d5741..a65fb07d 100644
--- a/src/lib/searchIndexManager.ts
+++ b/src/lib/searchIndex.ts
@@ -9,65 +9,26 @@
  * https://github.com/zhukov/webogram/blob/master/LICENSE
  */
 
-import Config from './config';
+import cleanSearchText from '../helpers/cleanSearchText';
 
-export type SearchIndex = {
-  fullTexts: {
-    [peerId: string]: string
-  }/* ,
-  shortIndexes: {
-    [shortStr: string]: number[]
-  } */
-};
+export default class SearchIndex<SearchWhat> {
+  private fullTexts: Map<SearchWhat, string> = new Map();
 
-class SearchIndexManager {
-  public static badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<]+/g;
-  public static trimRe = /^\s+|\s$/g;
-
-  public createIndex(): SearchIndex {
-    return {
-      fullTexts: {}/* ,
-      shortIndexes: {} */
-    };
-  }
-
-  public cleanSearchText(text: string, latinize = true) {
-    const hasTag = text.charAt(0) === '%';
-    text = text.replace(SearchIndexManager['badCharsRe'], '').replace(SearchIndexManager['trimRe'], '');
-    if(latinize) {
-      text = text.replace(/[^A-Za-z0-9]/g, (ch) => {
-        const latinizeCh = Config.LatinizeMap[ch];
-        return latinizeCh !== undefined ? latinizeCh : ch;
-      });
-    }
-    
-    text = text.toLowerCase();
-    if(hasTag) {
-      text = '%' + text;
-    }
-
-    return text;
-  }
-
-  public cleanUsername(username: string) {
-    return username && username.toLowerCase() || '';
-  }
-
-  public indexObject(id: number, searchText: string, searchIndex: SearchIndex) {
+  public indexObject(id: SearchWhat, searchText: string) {
     /* if(searchIndex.fullTexts.hasOwnProperty(id)) {
       return false;
     } */
 
     if(searchText.trim()) {
-      searchText = this.cleanSearchText(searchText);
+      searchText = cleanSearchText(searchText);
     }
 
     if(!searchText) {
-      delete searchIndex.fullTexts[id];
+      this.fullTexts.delete(id);
       return false;
     }
 
-    searchIndex.fullTexts[id] = searchText;
+    this.fullTexts.set(id, searchText);
     
     /* const shortIndexes = searchIndex.shortIndexes;
     searchText.split(' ').forEach((searchWord) => {
@@ -84,17 +45,15 @@ class SearchIndexManager {
     }); */
   }
 
-  public search(query: string, searchIndex: SearchIndex) {
-    const fullTexts = searchIndex.fullTexts;
+  public search(query: string) {
+    const fullTexts = this.fullTexts;
     //const shortIndexes = searchIndex.shortIndexes;
 
-    query = this.cleanSearchText(query);
+    query = cleanSearchText(query);
 
-    const newFoundObjs: {[peerId: string]: true} = {};
+    const newFoundObjs: Array<{fullText: string, what: SearchWhat}> = [];
     const queryWords = query.split(' ');
-    for(const peerId in fullTexts) {
-      const fullText = fullTexts[peerId];
-
+    fullTexts.forEach((fullText, what) => {
       let found = true;
       for(const word of queryWords) { // * verify that all words are found
         const idx = fullText.indexOf(word);
@@ -105,10 +64,12 @@ class SearchIndexManager {
       }
 
       if(found) {
-        newFoundObjs[peerId] = true;
+        newFoundObjs.push({fullText, what});
       }
-    }
-    
+    });
+
+    //newFoundObjs.sort((a, b) => a.fullText.localeCompare(b.fullText));
+    const newFoundObjs2: Set<SearchWhat> = new Set(newFoundObjs.map(o => o.what));
 
     /* const queryWords = query.split(' ');
     let foundArr: number[];
@@ -139,8 +100,6 @@ class SearchIndexManager {
       }
     } */
 
-    return newFoundObjs;
+    return newFoundObjs2;
   }
 }
-
-export default new SearchIndexManager();
diff --git a/src/lib/sessionStorage.ts b/src/lib/sessionStorage.ts
index c91b5f63..57096ba0 100644
--- a/src/lib/sessionStorage.ts
+++ b/src/lib/sessionStorage.ts
@@ -7,6 +7,7 @@
 import type { ChatSavedPosition } from './appManagers/appImManager';
 import type { State } from './appManagers/appStateManager';
 import type { AppDraftsManager } from './appManagers/appDraftsManager';
+import type { AppInstance } from './mtproto/singleInstance';
 import { MOUNT_CLASS_TO } from '../config/debug';
 import { LangPackDifference } from '../layer';
 import AppStorage from './storage';
@@ -21,6 +22,7 @@ const sessionStorage = new AppStorage<{
   dc5_auth_key: any,
   max_seen_msg: number,
   server_time_offset: number,
+  xt_instance: AppInstance,
 
   chatPositions: {
     [peerId_threadId: string]: ChatSavedPosition
diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts
index 7f85f983..768023e9 100644
--- a/src/lib/storages/dialogs.ts
+++ b/src/lib/storages/dialogs.ts
@@ -20,7 +20,7 @@ import type { ApiUpdatesManager } from "../appManagers/apiUpdatesManager";
 import type { ServerTimeManager } from "../mtproto/serverTimeManager";
 import { tsNow } from "../../helpers/date";
 import apiManager from "../mtproto/mtprotoworker";
-import searchIndexManager from "../searchIndexManager";
+import SearchIndex from "../searchIndex";
 import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array";
 import rootScope from "../rootScope";
 import { safeReplaceObject } from "../../helpers/object";
@@ -38,7 +38,7 @@ export default class DialogsStorage {
   private pinnedOrders: {[folder_id: number]: number[]};
   private dialogsNum: number;
 
-  private dialogsIndex = searchIndexManager.createIndex();
+  private dialogsIndex = new SearchIndex<number>();
 
   private cachedResults: {
     query: string,
@@ -71,7 +71,7 @@ export default class DialogsStorage {
       const dialog = this.getDialogOnly(peerId);
       if(dialog) {
         const peerText = appPeersManager.getPeerSearchText(peerId);
-        searchIndexManager.indexObject(peerId, peerText, this.dialogsIndex);
+        this.dialogsIndex.indexObject(peerId, peerText);
       }
     });
 
@@ -340,7 +340,7 @@ export default class DialogsStorage {
     if(foundDialog[0]) {
       this.byFolders[foundDialog[0].folder_id].splice(foundDialog[1], 1);
       delete this.dialogs[peerId];
-      searchIndexManager.indexObject(peerId, '', this.dialogsIndex);
+      this.dialogsIndex.indexObject(peerId, '');
 
       // clear from state
       this.appStateManager.keepPeerSingle(0, 'topMessage_' + peerId);
@@ -435,7 +435,7 @@ export default class DialogsStorage {
     }
 
     const peerText = this.appPeersManager.getPeerSearchText(peerId);
-    searchIndexManager.indexObject(peerId, peerText, this.dialogsIndex);
+    this.dialogsIndex.indexObject(peerId, peerText);
 
     let mid: number, message;
     if(dialog.top_message) {
@@ -537,13 +537,13 @@ export default class DialogsStorage {
         this.cachedResults.query = query;
         this.cachedResults.folderId = folderId;
 
-        const results = searchIndexManager.search(query, this.dialogsIndex);
+        const results = this.dialogsIndex.search(query);
 
         this.cachedResults.dialogs = [];
 
         for(const peerId in this.dialogs) {
           const dialog = this.dialogs[peerId];
-          if(results[dialog.peerId] && dialog.folder_id === folderId) {
+          if(results.has(dialog.peerId) && dialog.folder_id === folderId) {
             this.cachedResults.dialogs.push(dialog);
           }
         }
diff --git a/src/scss/partials/_chatEmojiHelper.scss b/src/scss/partials/_chatEmojiHelper.scss
new file mode 100644
index 00000000..0205f6c8
--- /dev/null
+++ b/src/scss/partials/_chatEmojiHelper.scss
@@ -0,0 +1,19 @@
+.emoji-helper {
+  height: 50px;
+  padding: .25rem !important;
+
+  > .scrollable {
+    position: relative;
+  }
+
+  .super-emojis {
+    display: block;
+    white-space: nowrap;
+  }
+
+  .super-emoji:not(.active) {
+		@include hover() {
+			background: none;
+		}
+	}
+}
diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss
index ef8547d4..fa79b391 100644
--- a/src/scss/partials/_emojiDropdown.scss
+++ b/src/scss/partials/_emojiDropdown.scss
@@ -163,65 +163,6 @@
 		position: relative;
 		margin: 0 -.125rem;
 
-		.category-items {
-			// ! No chrome 56 support
-			display: grid;
-			grid-column-gap: 2.44px;
-			grid-template-columns: repeat(auto-fill, 42px);
-			justify-content: space-between;
-
-			font-size: 2.25rem;
-			line-height: 2.25rem;
-
-			.category-item {
-				display: inline-block;
-				margin: 0 1.44px; // ! magic number
-				padding: 4px 4px;
-				line-height: inherit;
-				border-radius: 8px;
-				cursor: pointer;
-				user-select: none;
-				
-				width: 42px;
-				height: 42px;
-
-				html:not(.emoji-supported) & {
-					position: relative;
-				}
-
-				.emoji-placeholder {
-					position: absolute;
-					left: 7px;
-					top: 7px;
-					width: 1.75rem;
-					height: 1.75rem;
-					border-radius: 50%;
-					background-color: var(--light-secondary-text-color);
-
-					@include animation-level(2) {
-						opacity: 0;
-						transition: opacity .2s ease-in-out;
-					}
-				}
-
-				@include animation-level(2) {
-					img {
-						opacity: 1;
-						transition: opacity .2s ease-in-out;
-					}
-				}
-				
-				.emoji {
-					width: 100%;
-					height: 100%;
-					vertical-align: unset;
-					margin: 0;
-				}
-				
-				@include hover-background-effect();
-			}
-		}
-
 		/* &:first-child {
 			//padding-top: 5px;
 		} */
diff --git a/src/scss/style.scss b/src/scss/style.scss
index ce51cef7..cdb3c988 100644
--- a/src/scss/style.scss
+++ b/src/scss/style.scss
@@ -235,6 +235,7 @@ html.night {
 @import "partials/chatPinned";
 @import "partials/chatMarkupTooltip";
 @import "partials/chatStickersHelper";
+@import "partials/chatEmojiHelper";
 @import "partials/chatSearch";
 @import "partials/chatDrop";
 @import "partials/crop";
@@ -1213,3 +1214,62 @@ middle-ellipsis-element {
     border-radius: inherit;
   }
 }
+
+.super-emojis {
+  // ! No chrome 56 support
+  display: grid;
+  grid-column-gap: 2.44px;
+  grid-template-columns: repeat(auto-fill, 42px);
+  justify-content: space-between;
+
+  font-size: 2.25rem;
+  line-height: 2.25rem;
+
+  .super-emoji {
+    display: inline-block;
+    margin: 0 1.44px; // ! magic number
+    padding: 4px 4px;
+    line-height: inherit;
+    border-radius: 8px;
+    cursor: pointer;
+    user-select: none;
+    
+    width: 42px;
+    height: 42px;
+
+    html:not(.emoji-supported) & {
+      position: relative;
+    }
+
+    .emoji-placeholder {
+      position: absolute;
+      left: 7px;
+      top: 7px;
+      width: 1.75rem;
+      height: 1.75rem;
+      border-radius: 50%;
+      background-color: var(--light-secondary-text-color);
+
+      @include animation-level(2) {
+        opacity: 0;
+        transition: opacity .2s ease-in-out;
+      }
+    }
+
+    @include animation-level(2) {
+      img {
+        opacity: 1;
+        transition: opacity .2s ease-in-out;
+      }
+    }
+    
+    .emoji {
+      width: 100%;
+      height: 100%;
+      vertical-align: unset;
+      margin: 0;
+    }
+    
+    @include hover-background-effect();
+  }
+}