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(); + } +}