From 0f7522e32af5d1d0cca4286cdd91b25ce6fed391 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Fri, 23 Jul 2021 22:16:17 +0300 Subject: [PATCH] Fix autocomplete regex Increase download chunks limit --- src/components/appSearchSuper..ts | 2 +- src/components/chat/chat.ts | 2 +- src/components/chat/commandsHelper.ts | 4 +- src/components/chat/input.ts | 57 +++++++++++++++++------- src/components/chat/mentionsHelper.ts | 4 +- src/components/sidebarLeft/index.ts | 2 +- src/helpers/cleanSearchText.ts | 40 ++++++++++++----- src/lib/appManagers/appDialogsManager.ts | 2 +- src/lib/appManagers/appEmojiManager.ts | 2 +- src/lib/appManagers/appProfileManager.ts | 28 +++++++++--- src/lib/appManagers/appStateManager.ts | 11 +++-- src/lib/appManagers/appUsersManager.ts | 42 +++++++++++------ src/lib/mtproto/apiFileManager.ts | 2 +- src/lib/searchIndex.ts | 13 +++--- src/lib/storages/dialogs.ts | 7 ++- src/scss/partials/_chat.scss | 6 +++ 16 files changed, 160 insertions(+), 64 deletions(-) diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index 95791625..67c852e3 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -823,7 +823,7 @@ export default class AppSearchSuper { }; return Promise.all([ - appUsersManager.getTopPeers().then(peers => { + appUsersManager.getTopPeers('correspondents').then(peers => { if(!middleware()) return; const idx = peers.indexOf(rootScope.myId); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index c24ffb44..71c09324 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -173,7 +173,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.appPeersManager, this.appProfileManager); - 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.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.appUsersManager); this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager); diff --git a/src/components/chat/commandsHelper.ts b/src/components/chat/commandsHelper.ts index c02b06c8..203ed599 100644 --- a/src/components/chat/commandsHelper.ts +++ b/src/components/chat/commandsHelper.ts @@ -41,7 +41,9 @@ export default class CommandsHelper extends AutocompletePeerHelper { } const botInfos: BotInfo.botInfo[] = [].concat(full.bot_info); - const index = new SearchIndex(false, false); + const index = new SearchIndex({ + ignoreCase: true + }); const commands: Map = new Map(); botInfos.forEach(botInfo => { diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 70fef626..927b6f2c 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -14,6 +14,7 @@ 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 { AppUsersManager } from '../../lib/appManagers/appUsersManager'; import type Chat from './chat'; import Recorder from '../../../public/recorder.min'; import { isTouchSupported } from "../../helpers/touchSupport"; @@ -75,7 +76,7 @@ type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply'; export default class ChatInput { // private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/; - private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?:(?:@|^\/)\S*)|(?::|[^:@\/])(?!.*[:@\/]).*)$/; + private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?:(?:@|^\/)\S*)|(?::|^[^:@\/])(?!.*[:@\/]).*)$/; public messageInput: HTMLElement; public messageInputField: InputField; private fileInput: HTMLInputElement; @@ -169,7 +170,8 @@ export default class ChatInput { private appDraftsManager: AppDraftsManager, private serverTimeManager: ServerTimeManager, private appNotificationsManager: AppNotificationsManager, - private appEmojiManager: AppEmojiManager + private appEmojiManager: AppEmojiManager, + private appUsersManager: AppUsersManager ) { this.listenerSetter = new ListenerSetter(); } @@ -448,7 +450,8 @@ export default class ChatInput { this.listenerSetter.add(rootScope)('settings_updated', () => { if(this.stickersHelper || this.emojiHelper) { - this.previousQuery = undefined; + // this.previousQuery = undefined; + this.previousQuery = ''; this.checkAutocomplete(); /* if(!rootScope.settings.stickers.suggest) { this.stickersHelper.checkEmoticon(''); @@ -1242,20 +1245,27 @@ export default class ChatInput { value = value.substr(0, caretPos); + if(this.previousQuery === value) { + return; + } + + this.previousQuery = value; + const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP); + let foundHelper: AutocompleteHelper; if(!matches) { - this.previousQuery = undefined; - this.autocompleteHelperController.hideOtherHelpers(); + foundHelper = this.checkInlineAutocomplete(value); + // this.previousQuery = undefined; + this.autocompleteHelperController.hideOtherHelpers(foundHelper); return; } - if(this.previousQuery === matches[0]) { + /* if(this.previousQuery === matches[0]) { return; } - this.previousQuery = matches[0]; + this.previousQuery = matches[0]; */ - let foundHelper: AutocompleteHelper; const entity = entities[0]; const query = matches[2]; @@ -1267,14 +1277,9 @@ export default class ChatInput { entity?._ === 'messageEntityEmoji' && entity.length === value.length && !entity.offset) { foundHelper = this.stickersHelper; this.stickersHelper.checkEmoticon(value); - } else - //let query = cleanSearchText(query); - - //console.log('autocomplete matches', matches); - - if(firstChar === '@') { // mentions + } else if(firstChar === '@') { // mentions const topMsgId = this.chat.threadId ? this.appMessagesManager.getServerMessageId(this.chat.threadId) : undefined; - if(this.mentionsHelper.checkQuery(query, this.chat.peerId, topMsgId)) { + if(this.mentionsHelper.checkQuery(query, this.chat.peerId > 0 ? 0 : this.chat.peerId, topMsgId)) { foundHelper = this.mentionsHelper; } } else if(!matches[1] && firstChar === '/') { // commands @@ -1286,11 +1291,33 @@ export default class ChatInput { foundHelper = this.emojiHelper; this.emojiHelper.checkQuery(query, firstChar); } + } else { + foundHelper = this.checkInlineAutocomplete(value); } this.autocompleteHelperController.hideOtherHelpers(foundHelper); } + private checkInlineAutocomplete(value: string): AutocompleteHelper { + return; + + const inlineMatch = value.match(/^@([a-zA-Z\\d_]{3,32})\s/); + if(inlineMatch) { + const username = inlineMatch[1]; + console.log('inline match username', username); + this.appUsersManager.resolveUsername(username).then(peer => { + if(peer._ === 'user') { + if(peer.bot_inline_placeholder) { + this.messageInput.dataset.inlinePlaceholder = peer.bot_inline_placeholder; + } + + console.log(peer); + } + }); + return; + } + } + private onBtnSendClick = (e: Event) => { cancelEvent(e); diff --git a/src/components/chat/mentionsHelper.ts b/src/components/chat/mentionsHelper.ts index 7a523322..5add7164 100644 --- a/src/components/chat/mentionsHelper.ts +++ b/src/components/chat/mentionsHelper.ts @@ -43,9 +43,9 @@ export default class MentionsHelper extends AutocompletePeerHelper { public checkQuery(query: string, peerId: number, topMsgId: number) { const trimmed = query.trim(); // check that there is no whitespace - if(peerId > 0 || query.length !== trimmed.length) return false; + if(query.length !== trimmed.length) return false; - this.appProfileManager.getMentions(-peerId, trimmed, topMsgId).then(peerIds => { + this.appProfileManager.getMentions(peerId ? -peerId : 0, trimmed, topMsgId).then(peerIds => { const username = trimmed.slice(1).toLowerCase(); this.render(peerIds.map(peerId => { const user = this.appUsersManager.getUser(peerId); diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 35706bc3..b95139c9 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -246,7 +246,7 @@ export class AppSidebarLeft extends SidebarSlider { this.archivedCount.classList.toggle('hide', !e.count); }); - appUsersManager.getTopPeers(); + appUsersManager.getTopPeers('correspondents'); appStateManager.getState().then(state => { const recentSearch = state.recentSearch || []; diff --git a/src/helpers/cleanSearchText.ts b/src/helpers/cleanSearchText.ts index cd8d6818..2c860e59 100644 --- a/src/helpers/cleanSearchText.ts +++ b/src/helpers/cleanSearchText.ts @@ -14,20 +14,40 @@ import Config from "../lib/config"; const badCharsRe = /[`~!@#$%^&*()\-_=+\[\]\\|{}'";:\/?.>,<]+/g; const trimRe = /^\s+|\s$/g; +export function clearBadCharsAndTrim(text: string) { + return text.replace(badCharsRe, '').replace(trimRe, ''); +} + +export function latinizeString(text: string) { + return text.replace(/[^A-Za-z0-9]/g, (ch) => { + const latinizeCh = Config.LatinizeMap[ch]; + return latinizeCh !== undefined ? latinizeCh : ch; + }); +} + 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 = clearBadCharsAndTrim(text); + if(latinize) text = latinizeString(text); text = text.toLowerCase(); - if(hasTag) { - text = '%' + text; - } + if(hasTag) text = '%' + text; + + return text; +} + +export type ProcessSearchTextOptions = Partial<{ + clearBadChars: boolean, + latinize: boolean, + ignoreCase: boolean, + includeTag: boolean +}>; +export function processSearchText(text: string, options: ProcessSearchTextOptions = {}) { + const hasTag = options.includeTag && text.charAt(0) === '%'; + if(options.clearBadChars) text = clearBadCharsAndTrim(text); + if(options.latinize) text = latinizeString(text); + if(options.ignoreCase) text = text.toLowerCase(); + if(hasTag) text = '%' + text; return text; } diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 60c5df48..7e478f8b 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -834,7 +834,7 @@ export class AppDialogsManager { appUsersManager.getContacts().then(users => { let key: LangPackKey, args: FormatterArguments; - if(users.length) { + if(users.length/* && false */) { key = 'ChatList.Main.EmptyPlaceholder.Subtitle'; args = [i18n('Contacts.Count', [users.length])]; } else { diff --git a/src/lib/appManagers/appEmojiManager.ts b/src/lib/appManagers/appEmojiManager.ts index a263bc0e..02619159 100644 --- a/src/lib/appManagers/appEmojiManager.ts +++ b/src/lib/appManagers/appEmojiManager.ts @@ -166,7 +166,7 @@ export class AppEmojiManager { public indexEmojis() { if(!this.index) { - this.index = new SearchIndex(false, false, 2); + this.index = new SearchIndex(undefined, 2); } for(const langCode in this.keywordLangPacks) { diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index fb0a9459..1aa94eee 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -397,11 +397,15 @@ export class AppProfileManager { public getMentions(chatId: number, query: string, threadId?: number): Promise { const processUserIds = (userIds: number[]) => { + const startsWithAt = query.charAt(0) === '@'; + if(startsWithAt) query = query.slice(1); /* const startsWithAt = query.charAt(0) === '@'; if(startsWithAt) query = query.slice(1); const index = new SearchIndex(!startsWithAt, !startsWithAt); */ - const index = new SearchIndex(true, true); + const index = new SearchIndex({ + ignoreCase: true + }); userIds.forEach(userId => { index.indexObject(userId, appUsersManager.getUserSearchText(userId)); }); @@ -409,19 +413,31 @@ export class AppProfileManager { return Array.from(index.search(query)); }; + let promise: Promise; if(appChatsManager.isChannel(chatId)) { - return this.getChannelParticipants(chatId, { + promise = this.getChannelParticipants(chatId, { _: 'channelParticipantsMentions', q: query, top_msg_id: threadId }, 50, 0).then(cP => { - return processUserIds(cP.participants.map(p => appChatsManager.getParticipantPeerId(p))); + return cP.participants.map(p => appChatsManager.getParticipantPeerId(p)); }); - } else { - return (this.getChatFull(chatId) as Promise).then(chatFull => { - return processUserIds((chatFull.participants as ChatParticipants.chatParticipants).participants.map(p => p.user_id)); + } else if(chatId) { + promise = (this.getChatFull(chatId) as Promise).then(chatFull => { + return (chatFull.participants as ChatParticipants.chatParticipants).participants.map(p => p.user_id); }); + } else { + promise = Promise.resolve([]); } + + return Promise.all([ + [],// appUsersManager.getTopPeers('bots_inline').catch(() => []), + promise + ]).then(results => { + const peerIds = results[0].concat(results[1]); + + return processUserIds(peerIds); + }); } public invalidateChannelParticipants(id: number) { diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 4c0b19c6..e8c7eae0 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -6,7 +6,7 @@ import type { Dialog } from './appMessagesManager'; import type { UserAuth } from '../mtproto/mtproto_config'; -import type { User } from './appUsersManager'; +import type { TopPeerType, User } from './appUsersManager'; import type { AuthState } from '../../types'; import type FiltersStorage from '../storages/filters'; import type DialogsStorage from '../storages/dialogs'; @@ -54,7 +54,12 @@ export type State = { maxSeenMsgId: number, stateCreatedTime: number, recentEmoji: string[], - topPeers: number[], + topPeersCache: { + [type in TopPeerType]?: { + peerIds: number[], + cachedTime: number + } + }, recentSearch: number[], version: typeof STATE_VERSION, authState: AuthState, @@ -103,7 +108,7 @@ export const STATE_INIT: State = { maxSeenMsgId: 0, stateCreatedTime: Date.now(), recentEmoji: [], - topPeers: [], + topPeersCache: {}, recentSearch: [], version: STATE_VERSION, authState: { diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 1e468234..80f5fcb0 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -16,7 +16,7 @@ import cleanSearchText from "../../helpers/cleanSearchText"; import cleanUsername from "../../helpers/cleanUsername"; import { tsNow } from "../../helpers/date"; import { safeReplaceObject, isObject } from "../../helpers/object"; -import { InputUser, User as MTUser, UserProfilePhoto, UserStatus } from "../../layer"; +import { Chat, InputUser, User as MTUser, UserProfilePhoto, UserStatus } from "../../layer"; import I18n, { i18n, LangPackKey } from "../langPack"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; @@ -33,6 +33,7 @@ import appStateManager from "./appStateManager"; // TODO: updateUserBlocked export type User = MTUser.user; +export type TopPeerType = 'correspondents' | 'bots_inline'; export class AppUsersManager { private storage = appStateManager.storages.users; @@ -44,7 +45,7 @@ export class AppUsersManager { private contactsList: Set; private updatedContactsList: boolean; - private getTopPeersPromise: Promise; + private getTopPeersPromises: {[type in TopPeerType]?: Promise}; constructor() { this.clear(true); @@ -182,7 +183,8 @@ export class AppUsersManager { this.usernames = {}; } - this.contactsIndex = new SearchIndex(); + this.getTopPeersPromises = {}; + this.contactsIndex = this.createSearchIndex(); this.contactsFillPromise = undefined; this.contactsList = new Set(); this.updatedContactsList = false; @@ -219,7 +221,7 @@ export class AppUsersManager { return this.contactsFillPromise || (this.contactsFillPromise = promise); } - public resolveUsername(username: string) { + public resolveUsername(username: string): Promise { if(username[0] === '@') { username = username.slice(1); } @@ -314,11 +316,20 @@ export class AppUsersManager { public testSelfSearch(query: string) { const user = this.getSelf(); - const index = new SearchIndex(); + const index = this.createSearchIndex(); index.indexObject(user.id, this.getUserSearchText(user.id)); return index.search(query).has(user.id); } + private createSearchIndex() { + return new SearchIndex({ + clearBadChars: true, + ignoreCase: true, + latinize: true, + includeTag: true + }); + } + public saveApiUsers(apiUsers: any[], override?: boolean) { apiUsers.forEach((user) => this.saveApiUser(user, override)); } @@ -721,19 +732,20 @@ export class AppUsersManager { }); } */ - public getTopPeers(): Promise { - if(this.getTopPeersPromise) return this.getTopPeersPromise; + public getTopPeers(type: TopPeerType): Promise { + if(this.getTopPeersPromises[type]) return this.getTopPeersPromises[type]; - return this.getTopPeersPromise = appStateManager.getState().then((state) => { - if(state?.topPeers?.length) { - return state.topPeers; + return this.getTopPeersPromises[type] = appStateManager.getState().then((state) => { + const cached = state.topPeersCache[type]; + if(cached?.peerIds?.length) { + return cached.peerIds; } return apiManager.invokeApi('contacts.getTopPeers', { - correspondents: true, + [type]: true, offset: 0, limit: 15, - hash: 0, + hash: 0 }).then((result) => { let peerIds: number[] = []; if(result._ === 'contacts.topPeers') { @@ -750,7 +762,11 @@ export class AppUsersManager { } } - appStateManager.pushToState('topPeers', peerIds); + state.topPeersCache[type] = { + peerIds, + cachedTime: Date.now() + }; + appStateManager.pushToState('topPeersCache', state.topPeersCache); return peerIds; }); diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index bfe5f9d5..2bb02a16 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -133,7 +133,7 @@ export class ApiFileManager { private downloadCheck(dcId: string | number) { const downloadPull = this.downloadPulls[dcId]; - const downloadLimit = dcId === 'upload' ? 24 : 24; + const downloadLimit = dcId === 'upload' ? 24 : 36; //const downloadLimit = Infinity; if(this.downloadActives[dcId] >= downloadLimit || !downloadPull || !downloadPull.length) { diff --git a/src/lib/searchIndex.ts b/src/lib/searchIndex.ts index 9fccb2a9..2311b722 100644 --- a/src/lib/searchIndex.ts +++ b/src/lib/searchIndex.ts @@ -9,14 +9,13 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -import cleanSearchText from '../helpers/cleanSearchText'; +import { processSearchText, ProcessSearchTextOptions } from '../helpers/cleanSearchText'; export default class SearchIndex { private fullTexts: Map = new Map(); // minChars can be 0 because it requires at least one word (one symbol) to be found - constructor(private cleanText = true, private latinize = true, private minChars: number = 0) { - + constructor(private options?: ProcessSearchTextOptions, private minChars = 0) { } public indexObject(id: SearchWhat, searchText: string) { @@ -24,8 +23,8 @@ export default class SearchIndex { return false; } */ - if(searchText.trim() && this.cleanText) { - searchText = cleanSearchText(searchText, this.latinize); + if(this.options && searchText.trim()) { + searchText = processSearchText(searchText, this.options); } if(!searchText) { @@ -54,8 +53,8 @@ export default class SearchIndex { const fullTexts = this.fullTexts; //const shortIndexes = searchIndex.shortIndexes; - if(this.cleanText) { - query = cleanSearchText(query, this.latinize); + if(this.options) { + query = processSearchText(query, this.options); } const newFoundObjs: Array<{fullText: string, fullTextLength: number, what: SearchWhat, foundChars: number}> = []; diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index fa4e2cf6..b0a30c55 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -133,7 +133,12 @@ export default class DialogsStorage { 1: [] }; this.dialogsNum = 0; - this.dialogsIndex = new SearchIndex(); + this.dialogsIndex = new SearchIndex({ + clearBadChars: true, + ignoreCase: true, + latinize: true, + includeTag: true + }); this.cachedResults = { query: '', count: 0, diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 36a22910..0e960c2a 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -142,6 +142,12 @@ $chat-helper-size: 39px; @include respond-to(handhelds) { max-height: 10rem; } + + &[data-inline-placeholder]:after { + content: attr(data-inline-placeholder); + color: #a2acb4; + pointer-events: none; + } } .toggle-emoticons {