Browse Source

Autocomplete bot commands

Search by hashtags
master
morethanwords 4 years ago
parent
commit
d8be5fc0ba
  1. 2
      src/components/appSearch.ts
  2. 38
      src/components/chat/autocompleteHelper.ts
  3. 28
      src/components/chat/autocompleteHelperController.ts
  4. 22
      src/components/chat/bubbles.ts
  5. 17
      src/components/chat/chat.ts
  6. 15
      src/components/chat/commandsHelper.ts
  7. 12
      src/components/chat/emojiHelper.ts
  8. 79
      src/components/chat/input.ts
  9. 4
      src/components/chat/search.ts
  10. 31
      src/components/chat/stickersHelper.ts
  11. 43
      src/components/chat/topbar.ts
  12. 6
      src/components/sidebarRight/tabs/search.ts
  13. 3
      src/layer.d.ts
  14. 52
      src/lib/appManagers/appImManager.ts
  15. 2
      src/lib/appManagers/appMessagesManager.ts
  16. 4
      src/lib/appManagers/appUsersManager.ts
  17. 13
      src/lib/richtextprocessor.ts
  18. 5
      src/scripts/in/schema_additional_params.json

2
src/components/appSearch.ts

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

38
src/components/chat/autocompleteHelper.ts

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

28
src/components/chat/autocompleteHelperController.ts

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

22
src/components/chat/bubbles.ts

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

17
src/components/chat/chat.ts

@ -37,6 +37,8 @@ import { fastRaf } from "../../helpers/schedulers";
import AppPrivateSearchTab from "../sidebarRight/tabs/search"; import AppPrivateSearchTab from "../sidebarRight/tabs/search";
import type { State } from "../../lib/appManagers/appStateManager"; import type { State } from "../../lib/appManagers/appStateManager";
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl"; import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl";
import mediaSizes from "../../helpers/mediaSizes";
import ChatSearch from "./search";
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled'; export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
@ -371,4 +373,19 @@ export default class Chat extends EventListenerBase<{
public isAnyGroup() { public isAnyGroup() {
return this.peerId === rootScope.myId || this.peerId === REPLIES_PEER_ID || this.appPeersManager.isAnyGroup(this.peerId); 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);
}
}
} }

15
src/components/chat/commandsHelper.ts

@ -1,15 +1,24 @@
import type ChatInput from "./input";
import { BotCommand } from "../../layer"; import { BotCommand } from "../../layer";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
import AvatarElement from "../avatar"; import AvatarElement from "../avatar";
import Scrollable from "../scrollable"; import Scrollable from "../scrollable";
import AutocompleteHelper from "./autocompleteHelper"; import AutocompleteHelper from "./autocompleteHelper";
import AutocompleteHelperController from "./autocompleteHelperController";
export default class CommandsHelper extends AutocompleteHelper { export default class CommandsHelper extends AutocompleteHelper {
private scrollable: Scrollable; private scrollable: Scrollable;
constructor(appendTo: HTMLElement) { constructor(appendTo: HTMLElement, controller: AutocompleteHelperController, private chatInput: ChatInput) {
super(appendTo, 'y', (target) => { 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'); this.container.classList.add('commands-helper');

12
src/components/chat/emojiHelper.ts

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

79
src/components/chat/input.ts

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

4
src/components/chat/search.ts

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

31
src/components/chat/stickersHelper.ts

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

43
src/components/chat/topbar.ts

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

6
src/components/sidebarRight/tabs/search.ts

@ -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(); const ret = super.open();
if(this.init) { if(this.init) {
this.init(); this.init();
this.init = null; this.init = null;
} }
query && (this.inputSearch.inputField.value = query);
if(this.peerId !== 0) { if(this.peerId !== 0) {
this.appSearch.beginSearch(this.peerId, this.threadId); this.appSearch.beginSearch(this.peerId, this.threadId);
return ret; return ret;
@ -64,7 +66,7 @@ export default class AppPrivateSearchTab extends SliderSuperTab {
new PopupDatePicker(new Date(), this.onDatePick).show(); new PopupDatePicker(new Date(), this.onDatePick).show();
}); });
} }
appSidebarRight.toggleSidebar(true); appSidebarRight.toggleSidebar(true);
return ret; return ret;
} }

3
src/layer.d.ts vendored

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

52
src/lib/appManagers/appImManager.ts

@ -243,12 +243,47 @@ export class AppImManager {
return false; return false;
}; };
}
private onHashChange = () => { (window as any).execBotCommand = (element: HTMLAnchorElement, e: Event) => {
const hash = location.hash; cancelEvent(null);
const splitted = hash.split('?');
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 parseUriParams(uri: string, splitted = uri.split('?')) {
if(!splitted[1]) { if(!splitted[1]) {
return; return;
} }
@ -258,6 +293,15 @@ export class AppImManager {
params[item.split('=')[0]] = decodeURIComponent(item.split('=')[1]); 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); this.log('hashchange', hash, splitted[0], params);
switch(splitted[0]) { switch(splitted[0]) {

2
src/lib/appManagers/appMessagesManager.ts

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

4
src/lib/appManagers/appUsersManager.ts

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

13
src/lib/richtextprocessor.ts

@ -214,11 +214,12 @@ namespace RichTextProcessor {
offset: matchIndex + (match[10] ? match[10].length : 0), offset: matchIndex + (match[10] ? match[10].length : 0),
length: match[11].length length: match[11].length
}); });
} else if(match[12]) { // Bot command } else if(match[13]) { // Bot command
entities.push({ entities.push({
_: 'messageEntityBotCommand', _: 'messageEntityBotCommand',
offset: matchIndex + (match[11] ? match[11].length : 0), 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 contextHashtag?: string
}> = {}) { }> = {}) {
if(!text || !text.length) { if(!text) {
return ''; return '';
} }
@ -512,7 +513,7 @@ namespace RichTextProcessor {
} }
case 'messageEntityBotCommand': { case 'messageEntityBotCommand': {
if(!(options.noLinks || options.noCommands || contextExternal)) { if(!(options.noLinks || options.noCommands || contextExternal) && !entity.unsafe) {
const entityText = text.substr(entity.offset, entity.length); const entityText = text.substr(entity.offset, entity.length);
let command = entityText.substr(1); let command = entityText.substr(1);
let bot: string | boolean; let bot: string | boolean;
@ -524,7 +525,7 @@ namespace RichTextProcessor {
bot = options.fromBot; 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; break;
@ -617,7 +618,7 @@ namespace RichTextProcessor {
if(contextUrl) { if(contextUrl) {
const entityText = text.substr(entity.offset, entity.length); const entityText = text.substr(entity.offset, entity.length);
const hashtag = entityText.substr(1); 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; break;

5
src/scripts/in/schema_additional_params.json

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

Loading…
Cancel
Save