diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 6beb0fcb..e0bfb2ea 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -73,7 +73,7 @@ const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this 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*))$/; public messageInput: HTMLElement; public messageInputField: InputField; private fileInput: HTMLInputElement; @@ -1144,56 +1144,65 @@ export default class ChatInput { this.updateSendBtn(); }; - public onEmojiSelected = (emoji: string, autocomplete: boolean) => { - if(autocomplete) { - const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput); - const pos = caretPos >= 0 ? caretPos : fullValue.length; - const prefix = fullValue.substr(0, pos); - const suffix = fullValue.substr(pos); - - const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP); - - const matchIndex = matches.index + (matches[0].length - matches[2].length); - const newPrefix = prefix.slice(0, matchIndex); - const newValue = newPrefix + emoji + suffix; - - // merge emojis - const hadEntities = RichTextProcessor.parseEntities(fullValue); - RichTextProcessor.mergeEntities(entities, hadEntities); - - const emojiEntity = RichTextProcessor.getEmojiEntityFromEmoji(emoji); - const addEntities: MessageEntity[] = [emojiEntity]; - emojiEntity.offset = matchIndex; - addEntities.push({ - _: 'messageEntityCaret', - length: 0, - offset: emojiEntity.offset + emojiEntity.length - }); - - // add offset to entities next to emoji - const diff = emojiEntity.length - matches[2].length; - entities.forEach(entity => { - if(entity.offset >= emojiEntity.offset) { - entity.offset += diff; - } - }); + public insertAtCaret(insertText: string, insertEntity?: MessageEntity) { + const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput); + const pos = caretPos >= 0 ? caretPos : fullValue.length; + const prefix = fullValue.substr(0, pos); + const suffix = fullValue.substr(pos); - RichTextProcessor.mergeEntities(entities, addEntities); + const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP); - //const saveExecuted = this.prepareDocumentExecute(); - // can't exec .value here because it will instantly check for autocomplete - this.messageInputField.setValueSilently(RichTextProcessor.wrapDraftText(newValue, {entities}), true); + const matchIndex = matches.index + (matches[0].length - matches[2].length); + const newPrefix = prefix.slice(0, matchIndex); + const newValue = newPrefix + insertText + suffix; - const caret = this.messageInput.querySelector('.composer-sel'); - setRichFocus(this.messageInput, caret); - caret.remove(); + // merge emojis + const hadEntities = RichTextProcessor.parseEntities(fullValue); + RichTextProcessor.mergeEntities(entities, hadEntities); - // but it's needed to be checked only here - this.onMessageInput(); + // max for additional whitespace + const insertLength = insertEntity ? Math.max(insertEntity.length, insertText.length) : insertText.length; + const addEntities: MessageEntity[] = []; + if(insertEntity) { + addEntities.push(insertEntity); + insertEntity.offset = matchIndex; + } - //saveExecuted(); + addEntities.push({ + _: 'messageEntityCaret', + length: 0, + offset: matchIndex + insertLength + }); + + // add offset to entities next to emoji + const diff = insertLength - matches[2].length; + entities.forEach(entity => { + if(entity.offset >= matchIndex) { + entity.offset += diff; + } + }); - //document.execCommand('insertHTML', true, RichTextProcessor.wrapEmojiText(emoji)); + RichTextProcessor.mergeEntities(entities, addEntities); + + //const saveExecuted = this.prepareDocumentExecute(); + // can't exec .value here because it will instantly check for autocomplete + this.messageInputField.setValueSilently(RichTextProcessor.wrapDraftText(newValue, {entities}), true); + + const caret = this.messageInput.querySelector('.composer-sel'); + setRichFocus(this.messageInput, caret); + caret.remove(); + + // but it's needed to be checked only here + this.onMessageInput(); + + //saveExecuted(); + + //document.execCommand('insertHTML', true, RichTextProcessor.wrapEmojiText(emoji)); + } + + public onEmojiSelected = (emoji: string, autocomplete: boolean) => { + if(autocomplete) { + this.insertAtCaret(emoji, RichTextProcessor.getEmojiEntityFromEmoji(emoji)); } }; @@ -1248,20 +1257,27 @@ export default class ChatInput { //console.log('autocomplete matches', matches); - /* if(firstChar === '@') { // mentions - if(this.chat.peerId < 0) { + if(firstChar === '@') { // mentions + const trimmed = query.trim(); // check that there is no whitespace + if(this.chat.peerId < 0 && query.length === trimmed.length) { foundHelper = this.mentionsHelper; - this.chat.appProfileManager.getMentions(-this.chat.peerId, query).then(peerIds => { + const topMsgId = this.chat.threadId ? this.appMessagesManager.getServerMessageId(this.chat.threadId) : undefined; + this.chat.appProfileManager.getMentions(-this.chat.peerId, trimmed, topMsgId).then(peerIds => { + const username = trimmed.slice(1).toLowerCase(); this.mentionsHelper.render(peerIds.map(peerId => { const user = this.chat.appUsersManager.getUser(peerId); + if(user.username && user.username.toLowerCase() === username) { // hide full matched suggestion + return; + } + return { peerId, description: user.username ? '@' + user.username : undefined }; - })); + }).filter(Boolean)); }); } - } else */if(!matches[1] && firstChar === '/') { // commands + } else if(!matches[1] && firstChar === '/') { // commands if(appUsersManager.isBot(this.chat.peerId)) { foundHelper = this.commandsHelper; this.chat.appProfileManager.getProfileByPeerId(this.chat.peerId).then(full => { diff --git a/src/components/chat/mentionsHelper.ts b/src/components/chat/mentionsHelper.ts index 130d30e3..ba0be516 100644 --- a/src/components/chat/mentionsHelper.ts +++ b/src/components/chat/mentionsHelper.ts @@ -5,9 +5,10 @@ */ import type ChatInput from "./input"; +import type { MessageEntity } from "../../layer"; import AutocompleteHelperController from "./autocompleteHelperController"; import AutocompletePeerHelper from "./autocompletePeerHelper"; -import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd"; +import appUsersManager from "../../lib/appManagers/appUsersManager"; export default class MentionsHelper extends AutocompletePeerHelper { constructor(appendTo: HTMLElement, controller: AutocompleteHelperController, private chatInput: ChatInput) { @@ -15,9 +16,22 @@ export default class MentionsHelper extends AutocompletePeerHelper { controller, 'mentions-helper', (target) => { - const innerHTML = target.querySelector(`.${AutocompletePeerHelper.BASE_CLASS_LIST_ELEMENT}-description`).innerHTML; - chatInput.messageInputField.value = innerHTML + ' '; - placeCaretAtEnd(chatInput.messageInput); + const user = appUsersManager.getUser(+(target as HTMLElement).dataset.peerId); + let str = '', entity: MessageEntity; + if(user.username) { + str = '@' + user.username; + } else { + str = user.first_name || user.last_name; + entity = { + _: 'messageEntityMentionName', + length: str.length, + offset: 0, + user_id: user.id + }; + } + + str += ' '; + chatInput.insertAtCaret(str, entity); } ); } diff --git a/src/components/chat/stickersHelper.ts b/src/components/chat/stickersHelper.ts index b204eeba..5e16ce4d 100644 --- a/src/components/chat/stickersHelper.ts +++ b/src/components/chat/stickersHelper.ts @@ -5,7 +5,6 @@ */ import mediaSizes from "../../helpers/mediaSizes"; -import { clamp } from "../../helpers/number"; import { MyDocument } from "../../lib/appManagers/appDocsManager"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import appStickersManager from "../../lib/appManagers/appStickersManager"; diff --git a/src/helpers/dom/getRichElementValue.ts b/src/helpers/dom/getRichElementValue.ts index 09b4bf50..c7e33c09 100644 --- a/src/helpers/dom/getRichElementValue.ts +++ b/src/helpers/dom/getRichElementValue.ts @@ -11,10 +11,10 @@ import { MessageEntity } from "../../layer"; -export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link'; +export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link' | 'mentionName'; export type MarkdownTag = { match: string, - entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl'; + entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl' | 'messageEntityMentionName'; }; export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { bold: { @@ -38,8 +38,12 @@ export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { entityName: 'messageEntityStrike' }, link: { - match: 'A', + match: 'A:not(.follow)', entityName: 'messageEntityTextUrl' + }, + mentionName: { + match: 'A.follow', + entityName: 'messageEntityMentionName' } }; @@ -63,11 +67,18 @@ export default function getRichElementValue(node: HTMLElement, lines: string[], if(closest && closest.getAttribute('contenteditable') === null) { if(tag.entityName === 'messageEntityTextUrl') { entities.push({ - _: tag.entityName as any, + _: tag.entityName, url: (parentElement as HTMLAnchorElement).href, offset: offset.offset, length: nodeValue.length }); + } else if(tag.entityName === 'messageEntityMentionName') { + entities.push({ + _: tag.entityName, + offset: offset.offset, + length: nodeValue.length, + user_id: +parentElement.dataset.follow + }); } else { entities.push({ _: tag.entityName as any, diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts index 495ab49a..acd15e53 100644 --- a/src/lib/appManagers/appDraftsManager.ts +++ b/src/lib/appManagers/appDraftsManager.ts @@ -206,7 +206,7 @@ export class AppDraftsManager { } if(entities?.length) { - params.entities = entities; + params.entities = appMessagesManager.getInputEntities(entities); } if(localDraft.pFlags.no_webpage) { diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index ed6adfb5..40be0b33 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -313,11 +313,11 @@ export class AppMessagesManager { } public getInputEntities(entities: MessageEntity[]) { - var sendEntites = copy(entities); - sendEntites.forEach((entity: any) => { + const sendEntites = copy(entities); + sendEntites.forEach((entity) => { if(entity._ === 'messageEntityMentionName') { - entity._ = 'inputMessageEntityMentionName'; - entity.user_id = appUsersManager.getUserInput(entity.user_id); + (entity as any as MessageEntity.inputMessageEntityMentionName)._ = 'inputMessageEntityMentionName'; + (entity as any as MessageEntity.inputMessageEntityMentionName).user_id = appUsersManager.getUserInput(entity.user_id); } }); return sendEntites; diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index c24d2a72..7d2fdaa8 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -388,15 +388,29 @@ export class AppProfileManager { }) as any; } - public getMentions(chatId: number, query: string): Promise { - return (this.getChatFull(chatId) as Promise).then(chatFull => { + public getMentions(chatId: number, query: string, threadId?: number): Promise { + const processUserIds = (userIds: number[]) => { const index = new SearchIndex(true, true); - (chatFull.participants as ChatParticipants.chatParticipants).participants.forEach(participant => { - index.indexObject(participant.user_id, appUsersManager.getUserSearchText(participant.user_id)); + userIds.forEach(userId => { + index.indexObject(userId, appUsersManager.getUserSearchText(userId)); }); return Array.from(index.search(query)); - }); + }; + + if(appChatsManager.isChannel(chatId)) { + return this.getChannelParticipants(chatId, { + _: 'channelParticipantsMentions', + q: query, + top_msg_id: threadId + }, 50, 0).then(cP => { + return processUserIds(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)); + }); + } } public invalidateChannelParticipants(id: number) { diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 1c7cc252..acf58a64 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -626,8 +626,8 @@ namespace RichTextProcessor { } case 'messageEntityMentionName': { - if(!options.noLinks) { - insertPart(entity, `'); + if(!(options.noLinks && !passEntities[entity._])) { + insertPart(entity, `'); } break; @@ -709,7 +709,8 @@ namespace RichTextProcessor { noLinks: true, wrappingDraft: true, passEntities: { - messageEntityTextUrl: true + messageEntityTextUrl: true, + messageEntityMentionName: true } }); }