Chat input autocomplete helper

Fix iOS 13 blank page
This commit is contained in:
morethanwords 2021-05-18 17:17:54 +03:00
parent f156557b47
commit 5505ef5b8b
24 changed files with 882 additions and 384 deletions

View File

@ -17,7 +17,6 @@ import appUsersManager from "../lib/appManagers/appUsersManager";
import { logger } from "../lib/logger";
import RichTextProcessor from "../lib/richtextprocessor";
import rootScope from "../lib/rootScope";
import searchIndexManager from "../lib/searchIndexManager";
import AppMediaViewer from "./appMediaViewer";
import { SearchGroup, SearchGroupType } from "./appSearch";
import { horizontalMenu } from "./horizontalMenu";
@ -39,6 +38,7 @@ import appSidebarRight from "./sidebarRight";
import mediaSizes from "../helpers/mediaSizes";
import appImManager from "../lib/appManagers/appImManager";
import positionElementByIndex from "../helpers/dom/positionElementByIndex";
import cleanSearchText from "../helpers/cleanSearchText";
//const testScroll = false;
@ -738,7 +738,7 @@ export default class AppSearchSuper {
});
if(showMembersCount && (peer.participants_count || peer.participants)) {
const regExp = new RegExp(`(${escapeRegExp(query)}|${escapeRegExp(searchIndexManager.cleanSearchText(query))})`, 'gi');
const regExp = new RegExp(`(${escapeRegExp(query)}|${escapeRegExp(cleanSearchText(query))})`, 'gi');
dom.titleSpan.innerHTML = dom.titleSpan.innerHTML.replace(regExp, '<i>$1</i>');
dom.lastMessageSpan.append(appChatsManager.getChatMembersString(-peerId));
} else if(peerId === rootScope.myId) {

View File

@ -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);

View File

@ -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);
}
}

View File

@ -12,6 +12,7 @@ import type { AppPeersManager } from '../../lib/appManagers/appPeersManager';
import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager";
import type { AppImManager } from '../../lib/appManagers/appImManager';
import type { AppDraftsManager, MyDraftMessage } from '../../lib/appManagers/appDraftsManager';
import type { AppEmojiManager } from '../../lib/appManagers/appEmojiManager';
import type { ServerTimeManager } from '../../lib/mtproto/serverTimeManager';
import type Chat from './chat';
import Recorder from '../../../public/recorder.min';
@ -57,50 +58,52 @@ import isSendShortcutPressed from '../../helpers/dom/isSendShortcutPressed';
import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
import { MarkdownType, markdownTags } from '../../helpers/dom/getRichElementValue';
import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret';
import searchIndexManager from '../../lib/searchIndexManager';
import cleanSearchText from '../../helpers/cleanSearchText';
import EmojiHelper from './emojiHelper';
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
let selId = 0;
export default class ChatInput {
public static AUTO_COMPLETE_REG_EXP = /(\s|^)(:|@|\/)([\S]*)$/;
public pageEl = document.getElementById('page-chats') as HTMLDivElement;
public static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*:).*|(?:(?:@|\/)(?:[\S]*)))$/;
public messageInput: HTMLElement;
public messageInputField: InputField;
public fileInput: HTMLInputElement;
public inputMessageContainer: HTMLDivElement;
public btnSend = document.getElementById('btn-send') as HTMLButtonElement;
public btnCancelRecord: HTMLButtonElement;
public lastUrl = '';
public lastTimeType = 0;
private fileInput: HTMLInputElement;
private inputMessageContainer: HTMLDivElement;
private btnSend: HTMLButtonElement;
private btnCancelRecord: HTMLButtonElement;
private lastUrl = '';
private lastTimeType = 0;
public chatInput: HTMLElement;
public inputContainer: HTMLElement;
private inputContainer: HTMLElement;
public rowsWrapper: HTMLDivElement;
private newMessageWrapper: HTMLDivElement;
private btnToggleEmoticons: HTMLButtonElement;
public btnSendContainer: HTMLDivElement;
private btnSendContainer: HTMLDivElement;
public attachMenu: HTMLButtonElement;
private attachMenu: HTMLButtonElement;
private attachMenuButtons: (ButtonMenuItemOptions & {verify: (peerId: number) => boolean})[];
public sendMenu: SendMenu;
private sendMenu: SendMenu;
public replyElements: {
private replyElements: {
container?: HTMLElement,
cancelBtn?: HTMLButtonElement,
titleEl?: HTMLElement,
subtitleEl?: HTMLElement
} = {};
public willSendWebPage: any = null;
public forwardingMids: number[] = [];
public forwardingFromPeerId: number = 0;
private willSendWebPage: any = null;
private forwardingMids: number[] = [];
private forwardingFromPeerId: number = 0;
public replyToMsgId: number;
public editMsgId: number;
public noWebPage: true;
private noWebPage: true;
public scheduleDate: number;
public sendSilent: true;
@ -127,23 +130,35 @@ export default class ChatInput {
readonly executedHistory: string[] = [];
private canUndoFromHTML = '';
public stickersHelper: StickersHelper;
public listenerSetter: ListenerSetter;
private emojiHelper: EmojiHelper;
private stickersHelper: StickersHelper;
private listenerSetter: ListenerSetter;
public pinnedControlBtn: HTMLButtonElement;
private pinnedControlBtn: HTMLButtonElement;
public goDownBtn: HTMLButtonElement;
public goDownUnreadBadge: HTMLElement;
public btnScheduled: HTMLButtonElement;
private goDownBtn: HTMLButtonElement;
private goDownUnreadBadge: HTMLElement;
private btnScheduled: HTMLButtonElement;
public saveDraftDebounced: () => void;
private saveDraftDebounced: () => void;
public fakeRowsWrapper: HTMLDivElement;
private fakeRowsWrapper: HTMLDivElement;
private fakePinnedControlBtn: HTMLElement;
public previousQuery: string;
private previousQuery: string;
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager, private appDraftsManager: AppDraftsManager, private serverTimeManager: ServerTimeManager, private appNotificationsManager: AppNotificationsManager) {
constructor(private chat: Chat,
private appMessagesManager: AppMessagesManager,
private appDocsManager: AppDocsManager,
private appChatsManager: AppChatsManager,
private appPeersManager: AppPeersManager,
private appWebPagesManager: AppWebPagesManager,
private appImManager: AppImManager,
private appDraftsManager: AppDraftsManager,
private serverTimeManager: ServerTimeManager,
private appNotificationsManager: AppNotificationsManager,
private appEmojiManager: AppEmojiManager
) {
this.listenerSetter = new ListenerSetter();
}
@ -348,6 +363,7 @@ export default class ChatInput {
this.newMessageWrapper.append(...[this.btnToggleEmoticons, this.inputMessageContainer, this.btnScheduled, this.attachMenu, this.recordTimeEl, this.fileInput].filter(Boolean));
this.rowsWrapper.append(this.replyElements.container);
this.emojiHelper = new EmojiHelper(this.rowsWrapper, this);
this.stickersHelper = new StickersHelper(this.rowsWrapper);
this.rowsWrapper.append(this.newMessageWrapper);
@ -818,6 +834,9 @@ export default class ChatInput {
}
}); */
this.listenerSetter.add(this.messageInput, 'input', this.onMessageInput);
this.listenerSetter.add(this.messageInput, 'keyup', () => {
this.checkAutocomplete();
});
if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
this.listenerSetter.add(this.messageInput, 'focusin', () => {
@ -1029,10 +1048,10 @@ export default class ChatInput {
const {value: richValue, entities: markdownEntities, caretPos} = getRichValueWithCaret(this.messageInputField.input);
//const entities = RichTextProcessor.parseEntities(value);
const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities);
const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities, true);
const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value));
this.chat.log('messageInput entities', richValue, value, markdownEntities, caretPos);
//this.chat.log('messageInput entities', richValue, value, markdownEntities, caretPos);
if(this.stickersHelper &&
rootScope.settings.stickers.suggest &&
@ -1119,99 +1138,132 @@ export default class ChatInput {
this.saveDraftDebounced();
}
this.checkAutocomplete(richValue, caretPos);
this.updateSendBtn();
};
private checkAutocomplete(value: string, markdownEntities: MessageEntity[], entities: MessageEntity[]) {
const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP);
if(matches) {
if(this.previousQuery == matches[0]) {
return
}
this.previousQuery = matches[0]
var query = searchIndexManager.cleanSearchText(matches[3])
public onEmojiSelected = (emoji: string, autocomplete: boolean) => {
if(autocomplete) {
const {value: fullValue, caretPos} = getRichValueWithCaret(this.messageInput);
const pos = caretPos >= 0 ? caretPos : fullValue.length;
const suffix = fullValue.substr(pos);
const prefix = fullValue.substr(0, pos);
const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP);
console.log(matches);
/* if (matches[2] == '@') { // mentions
if (this.mentions && this.mentions.index) {
if (query.length) {
var foundObject = SearchIndexManager.search(query, this.mentions.index)
var foundUsers = []
var user
for (var i = 0, length = this.mentions.users.length; i < length; i++) {
user = this.mentions.users[i]
if (foundObject[user.id]) {
foundUsers.push(user)
}
}
} else {
var foundUsers = this.mentions.users
}
if (foundUsers.length) {
this.showMentionSuggestions(foundUsers)
} else {
this.hideSuggestions()
}
} else {
this.hideSuggestions()
}
} else if (!matches[1] && matches[2] == '/') { // commands
if (this.commands && this.commands.index) {
if (query.length) {
var foundObject = SearchIndexManager.search(query, this.commands.index)
var foundCommands = []
var command
for (var i = 0, length = this.commands.list.length; i < length; i++) {
command = this.commands.list[i]
if (foundObject[command.value]) {
foundCommands.push(command)
}
}
} else {
var foundCommands = this.commands.list
}
if (foundCommands.length) {
this.showCommandsSuggestions(foundCommands)
} else {
this.hideSuggestions()
}
} else {
this.hideSuggestions()
}
} else *//* if(matches[2] === ':') { // emoji
if(value.match(/^\s*:(.+):\s*$/)) {
return;
}
EmojiHelper.getPopularEmoji((function (popular) {
if (query.length) {
var found = EmojiHelper.searchEmojis(query)
if (found.length) {
var popularFound = [],
code
var pos
for (var i = 0, len = popular.length; i < len; i++) {
code = popular[i].code
pos = found.indexOf(code)
if (pos >= 0) {
popularFound.push(code)
found.splice(pos, 1)
if (!found.length) {
break
}
}
}
this.showEmojiSuggestions(popularFound.concat(found))
} else {
this.hideSuggestions()
}
} else {
this.showEmojiSuggestions(popular)
}
}).bind(this))
const idx = matches.index + matches[1].length;
//const str =
/* var newValuePrefix
if(matches && matches[0]) {
newValuePrefix = prefix.substr(0, matches.index) + ':' + emoji[1] + ':'
} else {
newValuePrefix = prefix + ':' + emoji[1] + ':'
}
} else {
delete this.previousQuery
this.hideSuggestions() */
if(suffix.length) {
const html = this.getRichHtml(newValuePrefix) + '&nbsp;<span id="composer_sel' + ++selId + '"></span>' + this.getRichHtml(suffix)
this.richTextareaEl.html(html)
setRichFocus(textarea, $('#composer_sel' + this.selId)[0])
} else {
const html = this.getRichHtml(newValuePrefix) + '&nbsp;'
this.richTextareaEl.html(html)
setRichFocus(textarea)
} */
}
};
private checkAutocomplete(value?: string, caretPos?: number) {
return;
if(value === undefined) {
const r = getRichValueWithCaret(this.messageInputField.input, false);
value = r.value;
caretPos = r.caretPos;
}
if(caretPos === -1) {
caretPos = value.length;
}
value = value.substr(0, caretPos);
const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP);
if(!matches) {
delete this.previousQuery;
//this.hideSuggestions();
this.emojiHelper.toggle(true);
return;
}
if(this.previousQuery === matches[0]) {
return;
}
this.previousQuery = matches[0];
//let query = cleanSearchText(matches[2]);
//const firstChar = matches[2][0];
//console.log('autocomplete matches', matches);
/*if (matches[2] == '@') { // mentions
if (this.mentions && this.mentions.index) {
if (query.length) {
var foundObject = SearchIndexManager.search(query, this.mentions.index)
var foundUsers = []
var user
for (var i = 0, length = this.mentions.users.length; i < length; i++) {
user = this.mentions.users[i]
if (foundObject[user.id]) {
foundUsers.push(user)
}
}
} else {
var foundUsers = this.mentions.users
}
if (foundUsers.length) {
this.showMentionSuggestions(foundUsers)
} else {
this.hideSuggestions()
}
} else {
this.hideSuggestions()
}
} else if (!matches[1] && matches[2] == '/') { // commands
if (this.commands && this.commands.index) {
if (query.length) {
var foundObject = SearchIndexManager.search(query, this.commands.index)
var foundCommands = []
var command
for (var i = 0, length = this.commands.list.length; i < length; i++) {
command = this.commands.list[i]
if (foundObject[command.value]) {
foundCommands.push(command)
}
}
} else {
var foundCommands = this.commands.list
}
if (foundCommands.length) {
this.showCommandsSuggestions(foundCommands)
} else {
this.hideSuggestions()
}
} else {
this.hideSuggestions()
}
} else *//* if(firstChar === ':') */ { // emoji
if(value.match(/^\s*:(.+):\s*$/)) {
this.emojiHelper.toggle(true);
return;
}
this.appEmojiManager.getBothEmojiKeywords().then(() => {
const emojis = this.appEmojiManager.searchEmojis(matches[2]);
this.emojiHelper.renderEmojis(emojis);
//console.log(emojis);
});
}
}

View File

@ -17,8 +17,92 @@ import { putPreloader } from "../../misc";
import Scrollable from "../../scrollable";
import StickyIntersector from "../../stickyIntersector";
const loadedURLs: Set<string> = new Set();
export function appendEmoji(emoji: string, container: HTMLElement, prepend = false/* , unified = false */) {
//const emoji = details.unified;
//const emoji = (details.unified as string).split('-')
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
const spanEmoji = document.createElement('span');
spanEmoji.classList.add('super-emoji');
let kek: string;
/* if(unified) {
kek = RichTextProcessor.wrapRichText('_', {
entities: [{
_: 'messageEntityEmoji',
offset: 0,
length: emoji.split('-').length,
unicode: emoji
}]
});
} else { */
kek = RichTextProcessor.wrapEmojiText(emoji);
//}
/* if(!kek.includes('emoji')) {
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
return;
} */
//console.log(kek);
spanEmoji.innerHTML = kek;
if(spanEmoji.children.length > 1) {
const first = spanEmoji.firstElementChild;
spanEmoji.innerHTML = '';
spanEmoji.append(first);
}
if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) {
const image = spanEmoji.firstElementChild as HTMLImageElement;
image.setAttribute('loading', 'lazy');
const url = image.src;
if(!loadedURLs.has(url)) {
const placeholder = document.createElement('span');
placeholder.classList.add('emoji-placeholder');
if(rootScope.settings.animationsEnabled) {
image.style.opacity = '0';
placeholder.style.opacity = '1';
}
image.addEventListener('load', () => {
fastRaf(() => {
if(rootScope.settings.animationsEnabled) {
image.style.opacity = '';
placeholder.style.opacity = '';
}
spanEmoji.classList.remove('empty');
loadedURLs.add(url);
});
}, {once: true});
spanEmoji.append(placeholder);
}
}
//spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
//spanEmoji.setAttribute('emoji', emoji);
if(prepend) container.prepend(spanEmoji);
else container.appendChild(spanEmoji);
}
export function getEmojiFromElement(element: HTMLElement) {
if(element.nodeType === 3) return element.nodeValue;
if(element.tagName === 'SPAN' && !element.classList.contains('emoji')) {
element = element.firstElementChild as HTMLElement;
}
return element.getAttribute('alt') || element.innerText;
}
export default class EmojiTab implements EmoticonsTab {
public content: HTMLElement;
private content: HTMLElement;
private recent: string[] = [];
private recentItemsDiv: HTMLElement;
@ -26,8 +110,6 @@ export default class EmojiTab implements EmoticonsTab {
private scroll: Scrollable;
private stickyIntersector: StickyIntersector;
private loadedURLs: Set<string> = new Set();
init() {
this.content = document.getElementById('content-emoji') as HTMLDivElement;
@ -84,7 +166,7 @@ export default class EmojiTab implements EmoticonsTab {
titleDiv.append(i18n(category));
const itemsDiv = document.createElement('div');
itemsDiv.classList.add('category-items');
itemsDiv.classList.add('super-emojis');
div.append(titleDiv, itemsDiv);
@ -95,7 +177,7 @@ export default class EmojiTab implements EmoticonsTab {
emoji = emoji.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
this.appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */);
appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */);
/* if(category === 'Smileys & Emotion') {
console.log('appended emoji', emoji, itemsDiv.children[itemsDiv.childElementCount - 1].innerHTML, emojiUnicode(emoji));
@ -125,9 +207,9 @@ export default class EmojiTab implements EmoticonsTab {
]).then(() => {
preloader.remove();
this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.category-items');
this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.super-emojis');
for(const emoji of this.recent) {
this.appendEmoji(emoji, this.recentItemsDiv);
appendEmoji(emoji, this.recentItemsDiv);
}
this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length);
@ -151,95 +233,12 @@ export default class EmojiTab implements EmoticonsTab {
this.init = null;
}
private appendEmoji(emoji: string, container: HTMLElement, prepend = false/* , unified = false */) {
//const emoji = details.unified;
//const emoji = (details.unified as string).split('-')
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
const spanEmoji = document.createElement('span');
spanEmoji.classList.add('category-item');
let kek: string;
/* if(unified) {
kek = RichTextProcessor.wrapRichText('_', {
entities: [{
_: 'messageEntityEmoji',
offset: 0,
length: emoji.split('-').length,
unicode: emoji
}]
});
} else { */
kek = RichTextProcessor.wrapEmojiText(emoji);
//}
/* if(!kek.includes('emoji')) {
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
return;
} */
//console.log(kek);
spanEmoji.innerHTML = kek;
if(spanEmoji.children.length > 1) {
const first = spanEmoji.firstElementChild;
spanEmoji.innerHTML = '';
spanEmoji.append(first);
}
if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) {
const image = spanEmoji.firstElementChild as HTMLImageElement;
image.setAttribute('loading', 'lazy');
const url = image.src;
if(!this.loadedURLs.has(url)) {
const placeholder = document.createElement('span');
placeholder.classList.add('emoji-placeholder');
if(rootScope.settings.animationsEnabled) {
image.style.opacity = '0';
placeholder.style.opacity = '1';
}
image.addEventListener('load', () => {
fastRaf(() => {
if(rootScope.settings.animationsEnabled) {
image.style.opacity = '';
placeholder.style.opacity = '';
}
spanEmoji.classList.remove('empty');
this.loadedURLs.add(url);
});
}, {once: true});
spanEmoji.append(placeholder);
}
}
//spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
//spanEmoji.setAttribute('emoji', emoji);
if(prepend) container.prepend(spanEmoji);
else container.appendChild(spanEmoji);
}
private getEmojiFromElement(element: HTMLElement) {
if(element.nodeType === 3) return element.nodeValue;
if(element.tagName === 'SPAN' && !element.classList.contains('emoji')) {
element = element.firstElementChild as HTMLElement;
}
return element.getAttribute('alt') || element.innerText;
}
onContentClick = (e: MouseEvent) => {
let target = e.target as HTMLElement;
//if(target.tagName !== 'SPAN') return;
if(target.tagName === 'SPAN' && !target.classList.contains('emoji')) {
target = findUpClassName(target, 'category-item');
target = findUpClassName(target, 'super-emoji');
if(!target) {
return;
}
@ -253,15 +252,15 @@ export default class EmojiTab implements EmoticonsTab {
target.outerHTML;
// Recent
const emoji = this.getEmojiFromElement(target);
const emoji = getEmojiFromElement(target);
(Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => {
const _emoji = this.getEmojiFromElement(el);
const _emoji = getEmojiFromElement(el);
if(emoji === _emoji) {
el.remove();
}
});
const scrollHeight = this.recentItemsDiv.scrollHeight;
this.appendEmoji(emoji, this.recentItemsDiv, true);
//const scrollHeight = this.recentItemsDiv.scrollHeight;
appendEmoji(emoji, this.recentItemsDiv, true);
this.recent.findAndSplice(e => e === emoji);
this.recent.unshift(emoji);

View File

@ -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() {
}
}
}

View File

@ -24,8 +24,8 @@ import StickyIntersector from "../../stickyIntersector";
import { wrapSticker } from "../../wrappers";
export class SuperStickerRenderer {
lazyLoadQueue: LazyLoadQueueRepeat;
animatedDivs: Set<HTMLDivElement> = new Set();
public lazyLoadQueue: LazyLoadQueueRepeat;
private animatedDivs: Set<HTMLDivElement> = new Set();
constructor(private regularLazyLoadQueue: LazyLoadQueue, private group: string) {
this.lazyLoadQueue = new LazyLoadQueueRepeat(undefined, (target, visible) => {
@ -35,7 +35,7 @@ export class SuperStickerRenderer {
});
}
renderSticker(doc: MyDocument, div?: HTMLDivElement, loadPromises?: Promise<any>[]) {
public renderSticker(doc: MyDocument, div?: HTMLDivElement, loadPromises?: Promise<any>[]) {
if(!div) {
div = document.createElement('div');
div.classList.add('grid-item', 'super-sticker');
@ -63,7 +63,7 @@ export class SuperStickerRenderer {
return div;
}
checkAnimationContainer = (div: HTMLElement, visible: boolean) => {
private checkAnimationContainer = (div: HTMLElement, visible: boolean) => {
//console.error('checkAnimationContainer', div, visible);
const players = animationIntersector.getAnimations(div);
players.forEach(player => {
@ -75,7 +75,7 @@ export class SuperStickerRenderer {
});
};
processVisibleDiv = (div: HTMLElement) => {
private processVisibleDiv = (div: HTMLElement) => {
const docId = div.dataset.docId;
const doc = appDocsManager.getDoc(docId);
@ -107,7 +107,7 @@ export class SuperStickerRenderer {
return promise;
};
processInvisibleDiv = (div: HTMLElement) => {
public processInvisibleDiv = (div: HTMLElement) => {
const docId = div.dataset.docId;
const doc = appDocsManager.getDoc(docId);
@ -121,7 +121,7 @@ export class SuperStickerRenderer {
}
export default class StickersTab implements EmoticonsTab {
public content: HTMLElement;
private content: HTMLElement;
private stickersDiv: HTMLElement;
private stickerSets: {[id: string]: {
@ -381,4 +381,4 @@ export default class StickersTab implements EmoticonsTab {
onClose() {
}
}
}

View File

@ -0,0 +1,33 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import Config from "../lib/config";
const badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<]+/g;
const trimRe = /^\s+|\s$/g;
export default function cleanSearchText(text: string, latinize = true) {
const hasTag = text.charAt(0) === '%';
text = text.replace(badCharsRe, '').replace(trimRe, '');
if(latinize) {
text = text.replace(/[^A-Za-z0-9]/g, (ch) => {
const latinizeCh = Config.LatinizeMap[ch];
return latinizeCh !== undefined ? latinizeCh : ch;
});
}
text = text.toLowerCase();
if(hasTag) {
text = '%' + text;
}
return text;
}

View File

@ -0,0 +1,14 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
export default function cleanUsername(username: string) {
return username && username.toLowerCase() || '';
}

View File

@ -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();

View File

@ -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;
}

View File

@ -0,0 +1,185 @@
import App from "../../config/app";
import { MOUNT_CLASS_TO } from "../../config/debug";
import { validateInitObject } from "../../helpers/object";
import I18n from "../langPack";
import { isObject } from "../mtproto/bin_utils";
import apiManager from "../mtproto/mtprotoworker";
import SearchIndex from "../searchIndex";
import sessionStorage from "../sessionStorage";
type EmojiLangPack = {
keywords: {
[keyword: string]: string[],
},
version: number,
langCode: string
};
const EMOJI_LANG_PACK: EmojiLangPack = {
keywords: {},
version: 0,
langCode: App.langPackCode
};
export class AppEmojiManager {
private keywordLangPacks: {
[langCode: string]: EmojiLangPack
} = {};
private index: SearchIndex<EmojiLangPack['keywords'][keyof EmojiLangPack['keywords']]>;
private indexedLangPacks: {[langCode: string]: boolean} = {};
private getKeywordsPromises: {[langCode: string]: Promise<EmojiLangPack>} = {};
/* public getPopularEmoji() {
return sessionStorage.get('emojis_popular').then(popEmojis => {
var result = []
if (popEmojis && popEmojis.length) {
for (var i = 0, len = popEmojis.length; i < len; i++) {
result.push({code: popEmojis[i][0], rate: popEmojis[i][1]})
}
callback(result)
return
}
return sessionStorage.get('emojis_recent').then(recentEmojis => {
recentEmojis = recentEmojis || popular || []
var shortcut
var code
for (var i = 0, len = recentEmojis.length; i < len; i++) {
shortcut = recentEmojis[i]
if (Array.isArray(shortcut)) {
shortcut = shortcut[0]
}
if (shortcut && typeof shortcut === 'string') {
if (shortcut.charAt(0) == ':') {
shortcut = shortcut.substr(1, shortcut.length - 2)
}
if (code = shortcuts[shortcut]) {
result.push({code: code, rate: 1})
}
}
}
callback(result)
});
});
}
function pushPopularEmoji (code) {
getPopularEmoji(function (popularEmoji) {
var exists = false
var count = popularEmoji.length
var result = []
for (var i = 0; i < count; i++) {
if (popularEmoji[i].code == code) {
exists = true
popularEmoji[i].rate++
}
result.push([popularEmoji[i].code, popularEmoji[i].rate])
}
if (exists) {
result.sort(function (a, b) {
return b[1] - a[1]
})
} else {
if (result.length > 41) {
result = result.slice(0, 41)
}
result.push([code, 1])
}
ConfigStorage.set({emojis_popular: result})
})
} */
public getEmojiKeywords(langCode: string = App.langPackCode) {
const promise = this.getKeywordsPromises[langCode];
if(promise) {
return promise;
}
const storageKey: any = 'emojiKeywords_' + langCode;
return this.getKeywordsPromises[langCode] = sessionStorage.get(storageKey).then((pack: EmojiLangPack) => {
if(!isObject(pack)) {
pack = {} as any;
}
validateInitObject(EMOJI_LANG_PACK, pack);
// important
pack.langCode = langCode;
this.keywordLangPacks[langCode] = pack;
return apiManager.invokeApi('messages.getEmojiKeywordsDifference', {
lang_code: pack.langCode,
from_version: pack.version
}).then((keywordsDifference) => {
pack.version = keywordsDifference.version;
const packKeywords = pack.keywords;
const keywords = keywordsDifference.keywords;
for(let i = 0, length = keywords.length; i < length; ++i) {
const {keyword, emoticons} = keywords[i];
packKeywords[keyword] = emoticons;
}
sessionStorage.set({
[storageKey]: pack
});
return pack;
}, () => {
return pack;
});
});
}
public getBothEmojiKeywords() {
const promises: ReturnType<AppEmojiManager['getEmojiKeywords']>[] = [
this.getEmojiKeywords()
];
if(I18n.lastRequestedLangCode !== App.langPackCode) {
promises.push(this.getEmojiKeywords(I18n.lastRequestedLangCode));
}
return Promise.all(promises);
}
public indexEmojis() {
if(!this.index) {
this.index = new SearchIndex();
}
for(const langCode in this.keywordLangPacks) {
if(this.indexedLangPacks[langCode]) {
continue;
}
const pack = this.keywordLangPacks[langCode];
const keywords = pack.keywords;
for(const keyword in keywords) {
const emoticons = keywords[keyword];
this.index.indexObject(emoticons, keyword);
}
this.indexedLangPacks[langCode] = true;
}
}
public searchEmojis(q: string) {
this.indexEmojis();
//const perf = performance.now();
const set = this.index.search(q);
const flattened = Array.from(set).reduce((acc, v) => acc.concat(v), []);
const emojis = Array.from(new Set(flattened));
//console.log('searchEmojis', q, 'time', performance.now() - perf);
return emojis;
}
}
const appEmojiManager = new AppEmojiManager();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appEmojiManager = appEmojiManager);
export default appEmojiManager;

View File

@ -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));

View File

@ -11,6 +11,8 @@
import { formatPhoneNumber } from "../../components/misc";
import { MOUNT_CLASS_TO } from "../../config/debug";
import cleanSearchText from "../../helpers/cleanSearchText";
import cleanUsername from "../../helpers/cleanUsername";
import { tsNow } from "../../helpers/date";
import { safeReplaceObject, isObject } from "../../helpers/object";
import { InputUser, Update, User as MTUser, UserStatus } from "../../layer";
@ -21,7 +23,7 @@ import { REPLIES_PEER_ID } from "../mtproto/mtproto_config";
import serverTimeManager from "../mtproto/serverTimeManager";
import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
import searchIndexManager from "../searchIndexManager";
import SearchIndex from "../searchIndex";
import apiUpdatesManager from "./apiUpdatesManager";
import appChatsManager from "./appChatsManager";
import appPeersManager from "./appPeersManager";
@ -36,7 +38,7 @@ export class AppUsersManager {
private users: {[userId: number]: User} = {};
private usernames: {[username: string]: number} = {};
private contactsIndex = searchIndexManager.createIndex();
private contactsIndex = new SearchIndex<number>();
private contactsFillPromise: Promise<Set<number>>;
private contactsList: Set<number> = new Set();
private updatedContactsList = false;
@ -110,7 +112,7 @@ export class AppUsersManager {
rootScope.on('language_change', (e) => {
const userId = this.getSelf().id;
searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex);
this.contactsIndex.indexObject(userId, this.getUserSearchText(userId));
});
appStateManager.getState().then((state) => {
@ -207,7 +209,7 @@ export class AppUsersManager {
public pushContact(userId: number) {
this.contactsList.add(userId);
searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex);
this.contactsIndex.indexObject(userId, this.getUserSearchText(userId));
appStateManager.requestPeer(userId, 'contacts');
}
@ -233,8 +235,8 @@ export class AppUsersManager {
return this.fillContacts().then(_contactsList => {
let contactsList = [..._contactsList];
if(query) {
const results = searchIndexManager.search(query, this.contactsIndex);
const filteredContactsList = [...contactsList].filter(id => !!results[id]);
const results = this.contactsIndex.search(query);
const filteredContactsList = [...contactsList].filter(id => results.has(id));
contactsList = filteredContactsList;
}
@ -288,9 +290,9 @@ export class AppUsersManager {
public testSelfSearch(query: string) {
const user = this.getSelf();
const index = searchIndexManager.createIndex();
searchIndexManager.indexObject(user.id, this.getUserSearchText(user.id), index);
return !!searchIndexManager.search(query, index)[user.id];
const index = new SearchIndex();
index.indexObject(user.id, this.getUserSearchText(user.id));
return index.search(query).has(user.id);
}
public saveApiUsers(apiUsers: any[], override?: boolean) {
@ -320,11 +322,11 @@ export class AppUsersManager {
const fullName = user.first_name + ' ' + (user.last_name || '');
if(user.username) {
const searchUsername = searchIndexManager.cleanUsername(user.username);
const searchUsername = cleanUsername(user.username);
this.usernames[searchUsername] = userId;
}
user.sortName = user.pFlags.deleted ? '' : searchIndexManager.cleanSearchText(fullName, false);
user.sortName = user.pFlags.deleted ? '' : cleanSearchText(fullName, false);
user.initials = RichTextProcessor.getAbbreviation(fullName);

View File

@ -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();

View File

@ -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;

View File

@ -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();
} */

View File

@ -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() {

View File

@ -9,65 +9,26 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import Config from './config';
import cleanSearchText from '../helpers/cleanSearchText';
export type SearchIndex = {
fullTexts: {
[peerId: string]: string
}/* ,
shortIndexes: {
[shortStr: string]: number[]
} */
};
export default class SearchIndex<SearchWhat> {
private fullTexts: Map<SearchWhat, string> = new Map();
class SearchIndexManager {
public static badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<]+/g;
public static trimRe = /^\s+|\s$/g;
public createIndex(): SearchIndex {
return {
fullTexts: {}/* ,
shortIndexes: {} */
};
}
public cleanSearchText(text: string, latinize = true) {
const hasTag = text.charAt(0) === '%';
text = text.replace(SearchIndexManager['badCharsRe'], '').replace(SearchIndexManager['trimRe'], '');
if(latinize) {
text = text.replace(/[^A-Za-z0-9]/g, (ch) => {
const latinizeCh = Config.LatinizeMap[ch];
return latinizeCh !== undefined ? latinizeCh : ch;
});
}
text = text.toLowerCase();
if(hasTag) {
text = '%' + text;
}
return text;
}
public cleanUsername(username: string) {
return username && username.toLowerCase() || '';
}
public indexObject(id: number, searchText: string, searchIndex: SearchIndex) {
public indexObject(id: SearchWhat, searchText: string) {
/* if(searchIndex.fullTexts.hasOwnProperty(id)) {
return false;
} */
if(searchText.trim()) {
searchText = this.cleanSearchText(searchText);
searchText = cleanSearchText(searchText);
}
if(!searchText) {
delete searchIndex.fullTexts[id];
this.fullTexts.delete(id);
return false;
}
searchIndex.fullTexts[id] = searchText;
this.fullTexts.set(id, searchText);
/* const shortIndexes = searchIndex.shortIndexes;
searchText.split(' ').forEach((searchWord) => {
@ -84,17 +45,15 @@ class SearchIndexManager {
}); */
}
public search(query: string, searchIndex: SearchIndex) {
const fullTexts = searchIndex.fullTexts;
public search(query: string) {
const fullTexts = this.fullTexts;
//const shortIndexes = searchIndex.shortIndexes;
query = this.cleanSearchText(query);
query = cleanSearchText(query);
const newFoundObjs: {[peerId: string]: true} = {};
const newFoundObjs: Array<{fullText: string, what: SearchWhat}> = [];
const queryWords = query.split(' ');
for(const peerId in fullTexts) {
const fullText = fullTexts[peerId];
fullTexts.forEach((fullText, what) => {
let found = true;
for(const word of queryWords) { // * verify that all words are found
const idx = fullText.indexOf(word);
@ -105,10 +64,12 @@ class SearchIndexManager {
}
if(found) {
newFoundObjs[peerId] = true;
newFoundObjs.push({fullText, what});
}
}
});
//newFoundObjs.sort((a, b) => a.fullText.localeCompare(b.fullText));
const newFoundObjs2: Set<SearchWhat> = new Set(newFoundObjs.map(o => o.what));
/* const queryWords = query.split(' ');
let foundArr: number[];
@ -139,8 +100,6 @@ class SearchIndexManager {
}
} */
return newFoundObjs;
return newFoundObjs2;
}
}
export default new SearchIndexManager();

View File

@ -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

View File

@ -20,7 +20,7 @@ import type { ApiUpdatesManager } from "../appManagers/apiUpdatesManager";
import type { ServerTimeManager } from "../mtproto/serverTimeManager";
import { tsNow } from "../../helpers/date";
import apiManager from "../mtproto/mtprotoworker";
import searchIndexManager from "../searchIndexManager";
import SearchIndex from "../searchIndex";
import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array";
import rootScope from "../rootScope";
import { safeReplaceObject } from "../../helpers/object";
@ -38,7 +38,7 @@ export default class DialogsStorage {
private pinnedOrders: {[folder_id: number]: number[]};
private dialogsNum: number;
private dialogsIndex = searchIndexManager.createIndex();
private dialogsIndex = new SearchIndex<number>();
private cachedResults: {
query: string,
@ -71,7 +71,7 @@ export default class DialogsStorage {
const dialog = this.getDialogOnly(peerId);
if(dialog) {
const peerText = appPeersManager.getPeerSearchText(peerId);
searchIndexManager.indexObject(peerId, peerText, this.dialogsIndex);
this.dialogsIndex.indexObject(peerId, peerText);
}
});
@ -340,7 +340,7 @@ export default class DialogsStorage {
if(foundDialog[0]) {
this.byFolders[foundDialog[0].folder_id].splice(foundDialog[1], 1);
delete this.dialogs[peerId];
searchIndexManager.indexObject(peerId, '', this.dialogsIndex);
this.dialogsIndex.indexObject(peerId, '');
// clear from state
this.appStateManager.keepPeerSingle(0, 'topMessage_' + peerId);
@ -435,7 +435,7 @@ export default class DialogsStorage {
}
const peerText = this.appPeersManager.getPeerSearchText(peerId);
searchIndexManager.indexObject(peerId, peerText, this.dialogsIndex);
this.dialogsIndex.indexObject(peerId, peerText);
let mid: number, message;
if(dialog.top_message) {
@ -537,13 +537,13 @@ export default class DialogsStorage {
this.cachedResults.query = query;
this.cachedResults.folderId = folderId;
const results = searchIndexManager.search(query, this.dialogsIndex);
const results = this.dialogsIndex.search(query);
this.cachedResults.dialogs = [];
for(const peerId in this.dialogs) {
const dialog = this.dialogs[peerId];
if(results[dialog.peerId] && dialog.folder_id === folderId) {
if(results.has(dialog.peerId) && dialog.folder_id === folderId) {
this.cachedResults.dialogs.push(dialog);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
} */

View File

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