Mentions
Render mentionName in message input Draft: save mentionName Fix following by mentionName
This commit is contained in:
parent
7d92d50b55
commit
70285c9c26
@ -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 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);
|
||||
|
||||
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 + insertText + suffix;
|
||||
|
||||
// merge emojis
|
||||
const hadEntities = RichTextProcessor.parseEntities(fullValue);
|
||||
RichTextProcessor.mergeEntities(entities, hadEntities);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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));
|
||||
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 => {
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -206,7 +206,7 @@ export class AppDraftsManager {
|
||||
}
|
||||
|
||||
if(entities?.length) {
|
||||
params.entities = entities;
|
||||
params.entities = appMessagesManager.getInputEntities(entities);
|
||||
}
|
||||
|
||||
if(localDraft.pFlags.no_webpage) {
|
||||
|
@ -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;
|
||||
|
@ -388,15 +388,29 @@ export class AppProfileManager {
|
||||
}) as any;
|
||||
}
|
||||
|
||||
public getMentions(chatId: number, query: string): Promise<number[]> {
|
||||
return (this.getChatFull(chatId) as Promise<ChatFull.chatFull>).then(chatFull => {
|
||||
public getMentions(chatId: number, query: string, threadId?: number): Promise<number[]> {
|
||||
const processUserIds = (userIds: number[]) => {
|
||||
const index = new SearchIndex<number>(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<ChatFull.chatFull>).then(chatFull => {
|
||||
return processUserIds((chatFull.participants as ChatParticipants.chatParticipants).participants.map(p => p.user_id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public invalidateChannelParticipants(id: number) {
|
||||
|
@ -626,8 +626,8 @@ namespace RichTextProcessor {
|
||||
}
|
||||
|
||||
case 'messageEntityMentionName': {
|
||||
if(!options.noLinks) {
|
||||
insertPart(entity, `<a href="#/im?p=u${encodeURIComponent(entity.user_id)}" class="follow" data-follow="${entity.user_id}">`, '</a>');
|
||||
if(!(options.noLinks && !passEntities[entity._])) {
|
||||
insertPart(entity, `<a href="#/im?p=${encodeURIComponent(entity.user_id)}" class="follow" data-follow="${entity.user_id}">`, '</a>');
|
||||
}
|
||||
|
||||
break;
|
||||
@ -709,7 +709,8 @@ namespace RichTextProcessor {
|
||||
noLinks: true,
|
||||
wrappingDraft: true,
|
||||
passEntities: {
|
||||
messageEntityTextUrl: true
|
||||
messageEntityTextUrl: true,
|
||||
messageEntityMentionName: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user