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

22
src/components/chat/mentionsHelper.ts

@ -5,9 +5,10 @@ @@ -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 { @@ -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);
}
);
}

1
src/components/chat/stickersHelper.ts

@ -5,7 +5,6 @@ @@ -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";

19
src/helpers/dom/getRichElementValue.ts

@ -11,10 +11,10 @@ @@ -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} = { @@ -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[], @@ -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,

2
src/lib/appManagers/appDraftsManager.ts

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

8
src/lib/appManagers/appMessagesManager.ts

@ -313,11 +313,11 @@ export class AppMessagesManager { @@ -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;

24
src/lib/appManagers/appProfileManager.ts

@ -388,15 +388,29 @@ export class AppProfileManager { @@ -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) {

7
src/lib/richtextprocessor.ts

@ -626,8 +626,8 @@ namespace RichTextProcessor { @@ -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 { @@ -709,7 +709,8 @@ namespace RichTextProcessor {
noLinks: true,
wrappingDraft: true,
passEntities: {
messageEntityTextUrl: true
messageEntityTextUrl: true,
messageEntityMentionName: true
}
});
}

Loading…
Cancel
Save