Autocomplete bot commands

Search by hashtags
This commit is contained in:
morethanwords 2021-05-28 20:01:06 +03:00
parent 5d6ff773f1
commit 3df92bd6f3
18 changed files with 247 additions and 129 deletions

View File

@ -72,7 +72,7 @@ export default class AppSearch {
private query = '';
public listsContainer: HTMLDivElement = null;
private listsContainer: HTMLDivElement = null;
private peerId = 0; // 0 - means global
private threadId = 0;

View File

@ -6,10 +6,12 @@
import attachListNavigation from "../../helpers/dom/attachlistNavigation";
import EventListenerBase from "../../helpers/eventListenerBase";
import { safeAssign } from "../../helpers/object";
import { isMobile } from "../../helpers/userAgent";
import rootScope from "../../lib/rootScope";
import appNavigationController, { NavigationItem } from "../appNavigationController";
import SetTransition from "../singleTransition";
import AutocompleteHelperController from "./autocompleteHelperController";
export default class AutocompleteHelper extends EventListenerBase<{
hidden: () => void,
@ -21,19 +23,30 @@ export default class AutocompleteHelper extends EventListenerBase<{
protected resetTarget: () => void;
protected init?(): void;
constructor(appendTo: HTMLElement,
protected listType: 'xy' | 'x' | 'y',
protected onSelect: (target: Element) => boolean | void,
protected waitForKey?: string
) {
protected controller: AutocompleteHelperController;
protected listType: 'xy' | 'x' | 'y';
protected onSelect: (target: Element) => boolean | void;
protected waitForKey?: string;
constructor(options: {
appendTo: HTMLElement,
controller: AutocompleteHelper['controller'],
listType: AutocompleteHelper['listType'],
onSelect: AutocompleteHelper['onSelect'],
waitForKey?: AutocompleteHelper['waitForKey']
}) {
super(false);
safeAssign(this, options);
this.container = document.createElement('div');
this.container.classList.add('autocomplete-helper', 'z-depth-1');
appendTo.append(this.container);
options.appendTo.append(this.container);
this.attachNavigation();
this.controller.addHelper(this);
}
protected onVisible = () => {
@ -89,7 +102,14 @@ export default class AutocompleteHelper extends EventListenerBase<{
}
this.hidden = hide;
!this.hidden && this.dispatchEvent('visible'); // fire it before so target will be set
if(!this.hidden) {
this.controller.hideOtherHelpers(this);
this.dispatchEvent('visible'); // fire it before so target will be set
} else {
this.controller.hideOtherHelpers();
}
SetTransition(this.container, 'is-visible', !hide, rootScope.settings.animationsEnabled ? 200 : 0, () => {
this.hidden && this.dispatchEvent('hidden');
});

View File

@ -1,21 +1,33 @@
import { getMiddleware } from "../../helpers/middleware";
import AutocompleteHelper from "./autocompleteHelper";
export default class AutocompleteHelperController {
private helpers: Set<AutocompleteHelper> = new Set();
private middleware = getMiddleware();
/* private tempId = 0;
public addHelpers(helpers: AutocompleteHelper[]) {
for(const helper of helpers) {
this.helpers.add(helper);
}
public incrementToggleCount() {
return ++this.tempId;
}
public toggleHelper(helper: AutocompleteHelper, hide?: boolean) {
public getToggleCount() {
return this.tempId;
} */
public getMiddleware() {
this.middleware.clean();
return this.middleware.get();
}
public addHelper(helper: AutocompleteHelper) {
this.helpers.add(helper);
}
public hideOtherHelpers(helper?: AutocompleteHelper) {
this.helpers.forEach(h => {
if(h !== helper) {
helper.toggle(true);
h.toggle(true);
}
});
helper.toggle(hide);
}
}

View File

@ -78,9 +78,9 @@ let TEST_SCROLL = TEST_SCROLL_TIMES;
let queueId = 0;
export default class ChatBubbles {
bubblesContainer: HTMLDivElement;
chatInner: HTMLDivElement;
scrollable: Scrollable;
public bubblesContainer: HTMLDivElement;
private chatInner: HTMLDivElement;
public scrollable: Scrollable;
private getHistoryTopPromise: Promise<boolean>;
private getHistoryBottomPromise: Promise<boolean>;
@ -88,11 +88,11 @@ export default class ChatBubbles {
public peerId = 0;
//public messagesCount: number = -1;
public unreadOut = new Set<number>();
private unreadOut = new Set<number>();
public needUpdate: {replyToPeerId: number, replyMid: number, mid: number}[] = []; // if need wrapSingleMessage
public bubbles: {[mid: string]: HTMLDivElement} = {};
public dateMessages: {[timestamp: number]: {
private dateMessages: {[timestamp: number]: {
div: HTMLDivElement,
firstTimestamp: number,
container: HTMLDivElement,
@ -109,14 +109,14 @@ export default class ChatBubbles {
private unreadedSeen: Set<number> = new Set();
private readPromise: Promise<void>;
public bubbleGroups: BubbleGroups;
private bubbleGroups: BubbleGroups;
private preloader: ProgressivePreloader = null;
private loadedTopTimes = 0;
private loadedBottomTimes = 0;
public messagesQueuePromise: Promise<void> = null;
private messagesQueuePromise: Promise<void> = null;
private messagesQueue: {message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise<void>[]}[] = [];
private messagesQueueOnRender: () => void = null;
private messagesQueueOnRenderAdditional: () => void = null;
@ -132,12 +132,12 @@ export default class ChatBubbles {
public listenerSetter: ListenerSetter;
public replyFollowHistory: number[] = [];
private replyFollowHistory: number[] = [];
public isHeavyAnimationInProgress = false;
public scrollingToNewBubble: HTMLElement;
private isHeavyAnimationInProgress = false;
private scrollingToNewBubble: HTMLElement;
public isFirstLoad = true;
private isFirstLoad = true;
private needReflowScroll: boolean;
private fetchNewPromise: Promise<void>;

View File

@ -37,6 +37,8 @@ import { fastRaf } from "../../helpers/schedulers";
import AppPrivateSearchTab from "../sidebarRight/tabs/search";
import type { State } from "../../lib/appManagers/appStateManager";
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl";
import mediaSizes from "../../helpers/mediaSizes";
import ChatSearch from "./search";
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
@ -371,4 +373,19 @@ export default class Chat extends EventListenerBase<{
public isAnyGroup() {
return this.peerId === rootScope.myId || this.peerId === REPLIES_PEER_ID || this.appPeersManager.isAnyGroup(this.peerId);
}
public initSearch(query?: string) {
if(!this.peerId) return;
if(mediaSizes.isMobile) {
new ChatSearch(this.topbar, this, query);
} else {
let tab = appSidebarRight.getTab(AppPrivateSearchTab);
if(!tab) {
tab = new AppPrivateSearchTab(appSidebarRight);
}
tab.open(this.peerId, this.threadId, this.bubbles.onDatePick, query);
}
}
}

View File

@ -1,15 +1,24 @@
import type ChatInput from "./input";
import { BotCommand } from "../../layer";
import RichTextProcessor from "../../lib/richtextprocessor";
import AvatarElement from "../avatar";
import Scrollable from "../scrollable";
import AutocompleteHelper from "./autocompleteHelper";
import AutocompleteHelperController from "./autocompleteHelperController";
export default class CommandsHelper extends AutocompleteHelper {
private scrollable: Scrollable;
constructor(appendTo: HTMLElement) {
super(appendTo, 'y', (target) => {
constructor(appendTo: HTMLElement, controller: AutocompleteHelperController, private chatInput: ChatInput) {
super({
appendTo,
controller,
listType: 'y',
onSelect: (target) => {
const command = target.querySelector('.commands-helper-command-name').innerHTML;
chatInput.messageInput.innerHTML = command;
chatInput.sendMessage();
}
});
this.container.classList.add('commands-helper');

View File

@ -2,13 +2,19 @@ import type ChatInput from "./input";
import { appendEmoji, getEmojiFromElement } from "../emoticonsDropdown/tabs/emoji";
import { ScrollableX } from "../scrollable";
import AutocompleteHelper from "./autocompleteHelper";
import AutocompleteHelperController from "./autocompleteHelperController";
export default class EmojiHelper extends AutocompleteHelper {
private scrollable: ScrollableX;
constructor(appendTo: HTMLElement, private chatInput: ChatInput) {
super(appendTo, 'x', (target) => {
this.chatInput.onEmojiSelected(getEmojiFromElement(target as any), true);
constructor(appendTo: HTMLElement, controller: AutocompleteHelperController, private chatInput: ChatInput) {
super({
appendTo,
controller,
listType: 'x',
onSelect: (target) => {
this.chatInput.onEmojiSelected(getEmojiFromElement(target as any), true);
}
});
this.container.classList.add('emoji-helper');

View File

@ -63,6 +63,8 @@ import setRichFocus from '../../helpers/dom/setRichFocus';
import SearchIndex from '../../lib/searchIndex';
import CommandsHelper from './commandsHelper';
import AutocompleteHelperController from './autocompleteHelperController';
import AutocompleteHelper from './autocompleteHelper';
import appUsersManager from '../../lib/appManagers/appUsersManager';
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -369,11 +371,9 @@ export default class ChatInput {
this.rowsWrapper.append(this.replyElements.container);
this.autocompleteHelperController = new AutocompleteHelperController();
this.autocompleteHelperController.addHelpers([
this.commandsHelper = new CommandsHelper(this.rowsWrapper),
this.emojiHelper = new EmojiHelper(this.rowsWrapper, this),
this.stickersHelper = new StickersHelper(this.rowsWrapper)
]);
this.stickersHelper = new StickersHelper(this.rowsWrapper, this.autocompleteHelperController);
this.emojiHelper = new EmojiHelper(this.rowsWrapper, this.autocompleteHelperController, this);
this.commandsHelper = new CommandsHelper(this.rowsWrapper, this.autocompleteHelperController, this);
this.rowsWrapper.append(this.newMessageWrapper);
this.btnCancelRecord = ButtonIcon('delete danger btn-circle z-depth-1 btn-record-cancel');
@ -433,11 +433,12 @@ export default class ChatInput {
this.listenerSetter.add(rootScope, 'settings_updated', () => {
if(this.stickersHelper) {
if(!rootScope.settings.stickers.suggest) {
this.checkAutocomplete();
/* if(!rootScope.settings.stickers.suggest) {
this.stickersHelper.checkEmoticon('');
} else {
this.onMessageInput();
}
} */
}
if(this.messageInputField) {
@ -1063,18 +1064,6 @@ export default class ChatInput {
//this.chat.log('messageInput entities', richValue, value, markdownEntities, caretPos);
if(this.stickersHelper &&
rootScope.settings.stickers.suggest &&
(this.chat.peerId > 0 || this.appChatsManager.hasRights(this.chat.peerId, 'send_stickers'))) {
let emoticon = '';
const entity = entities[0];
if(entity?._ === 'messageEntityEmoji' && entity.length === richValue.length && !entity.offset) {
emoticon = richValue;
}
this.stickersHelper.checkEmoticon(emoticon);
}
if(this.canRedoFromHTML && !this.lockRedo && this.messageInput.innerHTML !== this.canRedoFromHTML) {
this.canRedoFromHTML = '';
this.undoHistory.length = 0;
@ -1148,7 +1137,7 @@ export default class ChatInput {
this.saveDraftDebounced();
}
this.checkAutocomplete(richValue, caretPos);
this.checkAutocomplete(richValue, caretPos, entities);
this.updateSendBtn();
};
@ -1221,25 +1210,31 @@ export default class ChatInput {
}
};
private checkAutocomplete(value?: string, caretPos?: number) {
private checkAutocomplete(value?: string, caretPos?: number, entities?: MessageEntity[]) {
//return;
if(value === undefined) {
const r = getRichValueWithCaret(this.messageInputField.input, false);
const r = getRichValueWithCaret(this.messageInputField.input, true);
value = r.value;
caretPos = r.caretPos;
entities = r.entities;
}
if(caretPos === -1) {
caretPos = value.length;
}
if(entities === undefined) {
const _value = RichTextProcessor.parseMarkdown(value, entities, true);
entities = RichTextProcessor.mergeEntities(entities, RichTextProcessor.parseEntities(_value));
}
value = value.substr(0, caretPos);
const matches = value.match(ChatInput.AUTO_COMPLETE_REG_EXP);
if(!matches) {
delete this.previousQuery;
//this.hideSuggestions();
this.emojiHelper.toggle(true);
this.autocompleteHelperController.hideOtherHelpers();
return;
}
@ -1248,6 +1243,17 @@ export default class ChatInput {
}
this.previousQuery = matches[0];
let foundHelper: AutocompleteHelper;
const entity = entities[0];
if(this.stickersHelper &&
rootScope.settings.stickers.suggest &&
(this.chat.peerId > 0 || this.appChatsManager.hasRights(this.chat.peerId, 'send_stickers')) &&
entity?._ === 'messageEntityEmoji' && entity.length === value.length && !entity.offset) {
foundHelper = this.stickersHelper;
this.stickersHelper.checkEmoticon(value);
} else
//let query = cleanSearchText(matches[2]);
//const firstChar = matches[2][0];
@ -1277,7 +1283,8 @@ export default class ChatInput {
this.hideSuggestions()
}
} else */ if(!matches[1] && matches[2][0] === '/') { // commands
if(this.chat.peerId > 0) {
if(appUsersManager.isBot(this.chat.peerId)) {
foundHelper = this.commandsHelper;
this.chat.appProfileManager.getProfileByPeerId(this.chat.peerId).then(full => {
const botInfos: BotInfo.botInfo[] = [].concat(full.bot_info);
const index = new SearchIndex<string>(false, false);
@ -1293,22 +1300,22 @@ export default class ChatInput {
const found = index.search(matches[2]);
const filtered = Array.from(found).map(command => commands.get(command));
this.commandsHelper.render(filtered);
console.log('found commands', found, filtered);
// console.log('found commands', found, filtered);
});
}
} else { // emoji
if(value.match(/^\s*:(.+):\s*$/)) {
this.emojiHelper.toggle(true);
return;
if(!value.match(/^\s*:(.+):\s*$/)) {
foundHelper = this.emojiHelper;
this.appEmojiManager.getBothEmojiKeywords().then(() => {
const q = matches[2].replace(/^:/, '');
const emojis = this.appEmojiManager.searchEmojis(q);
this.emojiHelper.render(emojis, matches[2][0] !== ':');
//console.log(emojis);
});
}
this.appEmojiManager.getBothEmojiKeywords().then(() => {
const q = matches[2].replace(/^:/, '');
const emojis = this.appEmojiManager.searchEmojis(q);
this.emojiHelper.render(emojis, matches[2][0] !== ':');
//console.log(emojis);
});
}
this.autocompleteHelperController.hideOtherHelpers(foundHelper);
}
private onBtnSendClick = (e: Event) => {

View File

@ -37,7 +37,7 @@ export default class ChatSearch {
private selectedIndex = 0;
private setPeerPromise: Promise<any>;
constructor(private topbar: ChatTopbar, private chat: Chat) {
constructor(private topbar: ChatTopbar, private chat: Chat, private query?: string) {
this.element = document.createElement('div');
this.element.classList.add('sidebar-header', 'chat-search', 'chatlist-container');
@ -128,6 +128,8 @@ export default class ChatSearch {
this.topbar.container.parentElement.append(this.element);
this.inputSearch.input.focus();
query && (this.inputSearch.inputField.value = query);
}
onDateClick = (e: MouseEvent) => {

View File

@ -12,17 +12,23 @@ import { SuperStickerRenderer } from "../emoticonsDropdown/tabs/stickers";
import LazyLoadQueue from "../lazyLoadQueue";
import Scrollable from "../scrollable";
import AutocompleteHelper from "./autocompleteHelper";
import AutocompleteHelperController from "./autocompleteHelperController";
export default class StickersHelper extends AutocompleteHelper {
private scrollable: Scrollable;
private superStickerRenderer: SuperStickerRenderer;
private lazyLoadQueue: LazyLoadQueue;
private lastEmoticon = '';
constructor(appendTo: HTMLElement) {
super(appendTo, 'xy', (target) => {
EmoticonsDropdown.onMediaClick({target}, true);
}, 'ArrowUp');
constructor(appendTo: HTMLElement, controller: AutocompleteHelperController) {
super({
appendTo,
controller,
listType: 'xy',
onSelect: (target) => {
EmoticonsDropdown.onMediaClick({target}, true);
},
waitForKey: 'ArrowUp'
});
this.container.classList.add('stickers-helper');
@ -34,26 +40,15 @@ export default class StickersHelper extends AutocompleteHelper {
}
public checkEmoticon(emoticon: string) {
if(this.lastEmoticon === emoticon) return;
const middleware = this.controller.getMiddleware();
if(this.lastEmoticon && !emoticon) {
if(this.container) {
this.toggle(true);
}
}
this.lastEmoticon = emoticon;
if(this.lazyLoadQueue) {
this.lazyLoadQueue.clear();
}
if(!emoticon) {
return;
}
appStickersManager.getStickersByEmoticon(emoticon)
.then((stickers) => {
if(this.lastEmoticon !== emoticon) {
if(!middleware()) {
return;
}

View File

@ -19,14 +19,12 @@ import ButtonIcon from "../buttonIcon";
import ButtonMenuToggle from "../buttonMenuToggle";
import ChatAudio from "./audio";
import ChatPinnedMessage from "./pinnedMessage";
import ChatSearch from "./search";
import { ButtonMenuItemOptions } from "../buttonMenu";
import ListenerSetter from "../../helpers/listenerSetter";
import appStateManager from "../../lib/appManagers/appStateManager";
import PopupDeleteDialog from "../popups/deleteDialog";
import appNavigationController from "../appNavigationController";
import { LEFT_COLUMN_ACTIVE_CLASSNAME } from "../sidebarLeft";
import AppPrivateSearchTab from "../sidebarRight/tabs/search";
import PeerTitle from "../peerTitle";
import { i18n } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName";
@ -35,30 +33,30 @@ import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
export default class ChatTopbar {
container: HTMLDivElement;
btnBack: HTMLButtonElement;
chatInfo: HTMLDivElement;
avatarElement: AvatarElement;
title: HTMLDivElement;
subtitle: HTMLDivElement;
chatUtils: HTMLDivElement;
btnJoin: HTMLButtonElement;
btnPinned: HTMLButtonElement;
btnMute: HTMLButtonElement;
btnSearch: HTMLButtonElement;
btnMore: HTMLButtonElement;
public container: HTMLDivElement;
private btnBack: HTMLButtonElement;
private chatInfo: HTMLDivElement;
private avatarElement: AvatarElement;
private title: HTMLDivElement;
private subtitle: HTMLDivElement;
private chatUtils: HTMLDivElement;
private btnJoin: HTMLButtonElement;
private btnPinned: HTMLButtonElement;
private btnMute: HTMLButtonElement;
private btnSearch: HTMLButtonElement;
private btnMore: HTMLButtonElement;
public chatAudio: ChatAudio;
private chatAudio: ChatAudio;
public pinnedMessage: ChatPinnedMessage;
private setUtilsRAF: number;
public peerId: number;
public wasPeerId: number;
private wasPeerId: number;
private setPeerStatusInterval: number;
public listenerSetter: ListenerSetter;
public menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [];
private menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [];
constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private appNotificationsManager: AppNotificationsManager) {
this.listenerSetter = new ListenerSetter();
@ -189,7 +187,7 @@ export default class ChatTopbar {
icon: 'search',
text: 'Search',
onClick: () => {
new ChatSearch(this, this.chat);
this.chat.initSearch()
},
verify: () => mediaSizes.isMobile
}, /* {
@ -237,14 +235,7 @@ export default class ChatTopbar {
this.btnSearch = ButtonIcon('search');
attachClickEvent(this.btnSearch, (e) => {
cancelEvent(e);
if(this.peerId) {
let tab = this.appSidebarRight.getTab(AppPrivateSearchTab);
if(!tab) {
tab = new AppPrivateSearchTab(this.appSidebarRight);
}
tab.open(this.peerId, this.chat.threadId, this.chat.bubbles.onDatePick);
}
this.chat.initSearch();
}, {listenerSetter: this.listenerSetter});
}

View File

@ -42,13 +42,15 @@ export default class AppPrivateSearchTab extends SliderSuperTab {
});
}
open(peerId: number, threadId?: number, onDatePick?: AppPrivateSearchTab['onDatePick']) {
open(peerId: number, threadId?: number, onDatePick?: AppPrivateSearchTab['onDatePick'], query?: string) {
const ret = super.open();
if(this.init) {
this.init();
this.init = null;
}
query && (this.inputSearch.inputField.value = query);
if(this.peerId !== 0) {
this.appSearch.beginSearch(this.peerId, this.threadId);
return ret;
@ -64,7 +66,7 @@ export default class AppPrivateSearchTab extends SliderSuperTab {
new PopupDatePicker(new Date(), this.onDatePick).show();
});
}
appSidebarRight.toggleSidebar(true);
return ret;
}

3
src/layer.d.ts vendored
View File

@ -4049,7 +4049,8 @@ export namespace MessageEntity {
export type messageEntityBotCommand = {
_: 'messageEntityBotCommand',
offset: number,
length: number
length: number,
unsafe?: boolean
};
export type messageEntityUrl = {

View File

@ -243,12 +243,47 @@ export class AppImManager {
return false;
};
(window as any).execBotCommand = (element: HTMLAnchorElement, e: Event) => {
cancelEvent(null);
const href = element.href;
const params = this.parseUriParams(href);
if(!params) {
return;
}
const {command, bot} = params;
/* const promise = bot ? this.openUsername(bot).then(() => this.chat.peerId) : Promise.resolve(this.chat.peerId);
promise.then(peerId => {
appMessagesManager.sendText(peerId, '/' + command);
}); */
appMessagesManager.sendText(this.chat.peerId, '/' + command + (bot ? '@' + bot : ''));
//console.log(command, bot);
return false;
};
(window as any).searchByHashtag = (element: HTMLAnchorElement, e: Event) => {
cancelEvent(null);
const href = element.href;
const params = this.parseUriParams(href);
if(!params) {
return;
}
const {hashtag} = params;
this.chat.initSearch('#' + hashtag + ' ');
return false;
};
}
private onHashChange = () => {
const hash = location.hash;
const splitted = hash.split('?');
private parseUriParams(uri: string, splitted = uri.split('?')) {
if(!splitted[1]) {
return;
}
@ -258,6 +293,15 @@ export class AppImManager {
params[item.split('=')[0]] = decodeURIComponent(item.split('=')[1]);
});
return params;
}
private onHashChange = () => {
const hash = location.hash;
const splitted = hash.split('?');
const params = this.parseUriParams(hash, splitted);
this.log('hashchange', hash, splitted[0], params);
switch(splitted[0]) {

View File

@ -426,6 +426,7 @@ export class AppMessagesManager {
let entities = options.entities || [];
if(!options.viaBotId) {
text = RichTextProcessor.parseMarkdown(text, entities);
entities = RichTextProcessor.mergeEntities(entities, RichTextProcessor.parseEntities(text));
}
let sendEntites = this.getInputEntities(entities);
@ -2549,6 +2550,7 @@ export class AppMessagesManager {
let entities = RichTextProcessor.parseEntities(text.replace(/\n/g, ' '));
if(highlightWord) {
highlightWord = highlightWord.trim();
if(!entities) entities = [];
let found = false;
let match: any;

View File

@ -352,6 +352,10 @@ export class AppUsersManager {
changedTitle = true;
}
/* if(user.pFlags.bot && user.bot_info_version !== oldUser.bot_info_version) {
} */
safeReplaceObject(oldUser, user);
rootScope.broadcast('user_update', userId);
}

View File

@ -214,11 +214,12 @@ namespace RichTextProcessor {
offset: matchIndex + (match[10] ? match[10].length : 0),
length: match[11].length
});
} else if(match[12]) { // Bot command
} else if(match[13]) { // Bot command
entities.push({
_: 'messageEntityBotCommand',
offset: matchIndex + (match[11] ? match[11].length : 0),
length: 1 + match[12].length + (match[13] ? 1 + match[13].length : 0)
length: 1 + match[13].length + (match[14] ? 1 + match[14].length : 0),
unsafe: true
});
}
@ -417,7 +418,7 @@ namespace RichTextProcessor {
contextHashtag?: string
}> = {}) {
if(!text || !text.length) {
if(!text) {
return '';
}
@ -512,7 +513,7 @@ namespace RichTextProcessor {
}
case 'messageEntityBotCommand': {
if(!(options.noLinks || options.noCommands || contextExternal)) {
if(!(options.noLinks || options.noCommands || contextExternal) && !entity.unsafe) {
const entityText = text.substr(entity.offset, entity.length);
let command = entityText.substr(1);
let bot: string | boolean;
@ -524,7 +525,7 @@ namespace RichTextProcessor {
bot = options.fromBot;
}
insertPart(entity, `<a href="${encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : ''))}">`, `</a>`);
insertPart(entity, `<a href="${encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : ''))}" onclick="execBotCommand(this)">`, `</a>`);
}
break;
@ -617,7 +618,7 @@ namespace RichTextProcessor {
if(contextUrl) {
const entityText = text.substr(entity.offset, entity.length);
const hashtag = entityText.substr(1);
insertPart(entity, `<a class="anchor-hashtag" href="${contextUrl.replace('{1}', encodeURIComponent(hashtag))}"${contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ''}>`, '</a>');
insertPart(entity, `<a class="anchor-hashtag" href="${contextUrl.replace('{1}', encodeURIComponent(hashtag))}"${contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ' onclick="searchByHashtag(this)"'}>`, '</a>');
}
break;

View File

@ -101,6 +101,11 @@
{"name": "length", "type": "number"}
],
"type": "MessageEntity"
}, {
"predicate": "messageEntityBotCommand",
"params": [
{"name": "unsafe", "type": "boolean"}
]
}, {
"predicate": "user",
"params": [