Browse Source

Mentions

Render mentionName in message input
Draft: save mentionName
Fix following by mentionName
master
morethanwords 3 years ago
parent
commit
70285c9c26
  1. 116
      src/components/chat/input.ts
  2. 22
      src/components/chat/mentionsHelper.ts
  3. 1
      src/components/chat/stickersHelper.ts
  4. 19
      src/helpers/dom/getRichElementValue.ts
  5. 2
      src/lib/appManagers/appDraftsManager.ts
  6. 8
      src/lib/appManagers/appMessagesManager.ts
  7. 24
      src/lib/appManagers/appProfileManager.ts
  8. 7
      src/lib/richtextprocessor.ts

116
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'; type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
export default class ChatInput { export default class ChatInput {
private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*:).*|(?:(?:@|\/)(?:[\S]*)))$/; private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/;
public messageInput: HTMLElement; public messageInput: HTMLElement;
public messageInputField: InputField; public messageInputField: InputField;
private fileInput: HTMLInputElement; private fileInput: HTMLInputElement;
@ -1144,56 +1144,65 @@ export default class ChatInput {
this.updateSendBtn(); this.updateSendBtn();
}; };
public onEmojiSelected = (emoji: string, autocomplete: boolean) => { public insertAtCaret(insertText: string, insertEntity?: MessageEntity) {
if(autocomplete) { const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput);
const {value: fullValue, caretPos, entities} = getRichValueWithCaret(this.messageInput); const pos = caretPos >= 0 ? caretPos : fullValue.length;
const pos = caretPos >= 0 ? caretPos : fullValue.length; const prefix = fullValue.substr(0, pos);
const prefix = fullValue.substr(0, pos); const suffix = fullValue.substr(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;
}
});
RichTextProcessor.mergeEntities(entities, addEntities); const matches = prefix.match(ChatInput.AUTO_COMPLETE_REG_EXP);
//const saveExecuted = this.prepareDocumentExecute(); const matchIndex = matches.index + (matches[0].length - matches[2].length);
// can't exec .value here because it will instantly check for autocomplete const newPrefix = prefix.slice(0, matchIndex);
this.messageInputField.setValueSilently(RichTextProcessor.wrapDraftText(newValue, {entities}), true); const newValue = newPrefix + insertText + suffix;
const caret = this.messageInput.querySelector('.composer-sel'); // merge emojis
setRichFocus(this.messageInput, caret); const hadEntities = RichTextProcessor.parseEntities(fullValue);
caret.remove(); RichTextProcessor.mergeEntities(entities, hadEntities);
// but it's needed to be checked only here // max for additional whitespace
this.onMessageInput(); 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); //console.log('autocomplete matches', matches);
/* if(firstChar === '@') { // mentions if(firstChar === '@') { // mentions
if(this.chat.peerId < 0) { const trimmed = query.trim(); // check that there is no whitespace
if(this.chat.peerId < 0 && query.length === trimmed.length) {
foundHelper = this.mentionsHelper; 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 => { this.mentionsHelper.render(peerIds.map(peerId => {
const user = this.chat.appUsersManager.getUser(peerId); const user = this.chat.appUsersManager.getUser(peerId);
if(user.username && user.username.toLowerCase() === username) { // hide full matched suggestion
return;
}
return { return {
peerId, peerId,
description: user.username ? '@' + user.username : undefined 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)) { if(appUsersManager.isBot(this.chat.peerId)) {
foundHelper = this.commandsHelper; foundHelper = this.commandsHelper;
this.chat.appProfileManager.getProfileByPeerId(this.chat.peerId).then(full => { this.chat.appProfileManager.getProfileByPeerId(this.chat.peerId).then(full => {

22
src/components/chat/mentionsHelper.ts

@ -5,9 +5,10 @@
*/ */
import type ChatInput from "./input"; import type ChatInput from "./input";
import type { MessageEntity } from "../../layer";
import AutocompleteHelperController from "./autocompleteHelperController"; import AutocompleteHelperController from "./autocompleteHelperController";
import AutocompletePeerHelper from "./autocompletePeerHelper"; import AutocompletePeerHelper from "./autocompletePeerHelper";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd"; import appUsersManager from "../../lib/appManagers/appUsersManager";
export default class MentionsHelper extends AutocompletePeerHelper { export default class MentionsHelper extends AutocompletePeerHelper {
constructor(appendTo: HTMLElement, controller: AutocompleteHelperController, private chatInput: ChatInput) { constructor(appendTo: HTMLElement, controller: AutocompleteHelperController, private chatInput: ChatInput) {
@ -15,9 +16,22 @@ export default class MentionsHelper extends AutocompletePeerHelper {
controller, controller,
'mentions-helper', 'mentions-helper',
(target) => { (target) => {
const innerHTML = target.querySelector(`.${AutocompletePeerHelper.BASE_CLASS_LIST_ELEMENT}-description`).innerHTML; const user = appUsersManager.getUser(+(target as HTMLElement).dataset.peerId);
chatInput.messageInputField.value = innerHTML + ' '; let str = '', entity: MessageEntity;
placeCaretAtEnd(chatInput.messageInput); 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);
} }
); );
} }

1
src/components/chat/stickersHelper.ts

@ -5,7 +5,6 @@
*/ */
import mediaSizes from "../../helpers/mediaSizes"; import mediaSizes from "../../helpers/mediaSizes";
import { clamp } from "../../helpers/number";
import { MyDocument } from "../../lib/appManagers/appDocsManager"; import { MyDocument } from "../../lib/appManagers/appDocsManager";
import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import appStickersManager from "../../lib/appManagers/appStickersManager"; import appStickersManager from "../../lib/appManagers/appStickersManager";

19
src/helpers/dom/getRichElementValue.ts

@ -11,10 +11,10 @@
import { MessageEntity } from "../../layer"; 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 = { export type MarkdownTag = {
match: string, match: string,
entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl'; entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl' | 'messageEntityMentionName';
}; };
export const markdownTags: {[type in MarkdownType]: MarkdownTag} = { export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
bold: { bold: {
@ -38,8 +38,12 @@ export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
entityName: 'messageEntityStrike' entityName: 'messageEntityStrike'
}, },
link: { link: {
match: 'A', match: 'A:not(.follow)',
entityName: 'messageEntityTextUrl' 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(closest && closest.getAttribute('contenteditable') === null) {
if(tag.entityName === 'messageEntityTextUrl') { if(tag.entityName === 'messageEntityTextUrl') {
entities.push({ entities.push({
_: tag.entityName as any, _: tag.entityName,
url: (parentElement as HTMLAnchorElement).href, url: (parentElement as HTMLAnchorElement).href,
offset: offset.offset, offset: offset.offset,
length: nodeValue.length length: nodeValue.length
}); });
} else if(tag.entityName === 'messageEntityMentionName') {
entities.push({
_: tag.entityName,
offset: offset.offset,
length: nodeValue.length,
user_id: +parentElement.dataset.follow
});
} else { } else {
entities.push({ entities.push({
_: tag.entityName as any, _: tag.entityName as any,

2
src/lib/appManagers/appDraftsManager.ts

@ -206,7 +206,7 @@ export class AppDraftsManager {
} }
if(entities?.length) { if(entities?.length) {
params.entities = entities; params.entities = appMessagesManager.getInputEntities(entities);
} }
if(localDraft.pFlags.no_webpage) { if(localDraft.pFlags.no_webpage) {

8
src/lib/appManagers/appMessagesManager.ts

@ -313,11 +313,11 @@ export class AppMessagesManager {
} }
public getInputEntities(entities: MessageEntity[]) { public getInputEntities(entities: MessageEntity[]) {
var sendEntites = copy(entities); const sendEntites = copy(entities);
sendEntites.forEach((entity: any) => { sendEntites.forEach((entity) => {
if(entity._ === 'messageEntityMentionName') { if(entity._ === 'messageEntityMentionName') {
entity._ = 'inputMessageEntityMentionName'; (entity as any as MessageEntity.inputMessageEntityMentionName)._ = 'inputMessageEntityMentionName';
entity.user_id = appUsersManager.getUserInput(entity.user_id); (entity as any as MessageEntity.inputMessageEntityMentionName).user_id = appUsersManager.getUserInput(entity.user_id);
} }
}); });
return sendEntites; return sendEntites;

24
src/lib/appManagers/appProfileManager.ts

@ -388,15 +388,29 @@ export class AppProfileManager {
}) as any; }) as any;
} }
public getMentions(chatId: number, query: string): Promise<number[]> { public getMentions(chatId: number, query: string, threadId?: number): Promise<number[]> {
return (this.getChatFull(chatId) as Promise<ChatFull.chatFull>).then(chatFull => { const processUserIds = (userIds: number[]) => {
const index = new SearchIndex<number>(true, true); const index = new SearchIndex<number>(true, true);
(chatFull.participants as ChatParticipants.chatParticipants).participants.forEach(participant => { userIds.forEach(userId => {
index.indexObject(participant.user_id, appUsersManager.getUserSearchText(participant.user_id)); index.indexObject(userId, appUsersManager.getUserSearchText(userId));
}); });
return Array.from(index.search(query)); 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<ChatFull.chatFull>).then(chatFull => {
return processUserIds((chatFull.participants as ChatParticipants.chatParticipants).participants.map(p => p.user_id));
});
}
} }
public invalidateChannelParticipants(id: number) { public invalidateChannelParticipants(id: number) {

7
src/lib/richtextprocessor.ts

@ -626,8 +626,8 @@ namespace RichTextProcessor {
} }
case 'messageEntityMentionName': { case 'messageEntityMentionName': {
if(!options.noLinks) { if(!(options.noLinks && !passEntities[entity._])) {
insertPart(entity, `<a href="#/im?p=u${encodeURIComponent(entity.user_id)}" class="follow" data-follow="${entity.user_id}">`, '</a>'); insertPart(entity, `<a href="#/im?p=${encodeURIComponent(entity.user_id)}" class="follow" data-follow="${entity.user_id}">`, '</a>');
} }
break; break;
@ -709,7 +709,8 @@ namespace RichTextProcessor {
noLinks: true, noLinks: true,
wrappingDraft: true, wrappingDraft: true,
passEntities: { passEntities: {
messageEntityTextUrl: true messageEntityTextUrl: true,
messageEntityMentionName: true
} }
}); });
} }

Loading…
Cancel
Save