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, '$1');
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) + ' ' + this.getRichHtml(suffix)
+ this.richTextareaEl.html(html)
+ setRichFocus(textarea, $('#composer_sel' + this.selId)[0])
+ } else {
+ const html = this.getRichHtml(newValuePrefix) + ' '
+ 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 = 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 = 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 = new Set();
+ public lazyLoadQueue: LazyLoadQueueRepeat;
+ private animatedDivs: Set = 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[]) {
+ public renderSticker(doc: MyDocument, div?: HTMLDivElement, loadPromises?: Promise[]) {
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
+ * 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
+ * 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;
+ private indexedLangPacks: {[langCode: string]: boolean} = {};
+
+ private getKeywordsPromises: {[langCode: string]: Promise} = {};
+
+ /* 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[] = [
+ 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();
private contactsFillPromise: Promise>;
private contactsList: Set = 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 {
+ private fullTexts: Map = 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 = 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();
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();
+ }
+}