diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 83fcf834..015abbb8 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -36,13 +36,22 @@ export default class AppSelectPeers { private cachedContacts: number[]; private loadedWhat: Partial<{[k in 'dialogs' | 'archived' | 'contacts']: true}> = {}; + + private renderedPeerIDs: Set = new Set(); constructor(private appendTo: HTMLElement, private onChange?: (length: number) => void, private peerType: PeerType[] = ['dialogs'], onFirstRender?: () => void, private renderResultsFunc?: (peerIDs: number[]) => void, private chatRightsAction?: ChatRights, private multiSelect = true) { this.container.classList.add('selector'); - if(!this.renderResultsFunc) { - this.renderResultsFunc = this.renderResults; - } + const f = (renderResultsFunc || this.renderResults).bind(this); + this.renderResultsFunc = (peerIDs: number[]) => { + peerIDs = peerIDs.filter(peerID => { + const notRendered = !this.renderedPeerIDs.has(peerID); + if(notRendered) this.renderedPeerIDs.add(peerID); + return notRendered; + }); + + return f(peerIDs); + }; this.input = document.createElement('input'); this.input.classList.add('selector-search-input'); @@ -130,6 +139,7 @@ export default class AppSelectPeers { this.promise = null; this.list.innerHTML = ''; this.query = value; + this.renderedPeerIDs.clear(); //console.log('selectPeers input:', this.query); this.getMoreResults(); diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 3d21ac65..8477568e 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -1,13 +1,15 @@ +import { isTouchSupported } from "../../helpers/touchSupport"; import appChatsManager from "../../lib/appManagers/appChatsManager"; import appImManager from "../../lib/appManagers/appImManager"; import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../lib/appManagers/appPeersManager"; import appPollsManager, { Poll } from "../../lib/appManagers/appPollsManager"; import $rootScope from "../../lib/rootScope"; -import { findUpClassName } from "../../lib/utils"; +import { cancelEvent, cancelSelection, findUpClassName } from "../../lib/utils"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc"; import { PopupButton } from "../popup"; +import PopupDeleteMessages from "../popupDeleteMessages"; import PopupForward from "../popupForward"; import PopupPeer from "../popupPeer"; import appSidebarRight from "../sidebarRight"; @@ -21,7 +23,7 @@ export default class ChatContextMenu { public msgID: number; constructor(private attachTo: HTMLElement) { - attachContextMenuListener(attachTo, (e) => { + const onContextMenu = (e: MouseEvent | Touch) => { if(this.init) { this.init(); this.init = null; @@ -69,7 +71,29 @@ export default class ChatContextMenu { }); /////this.log('contextmenu', e, bubble, msgID, side); - }); + }; + + if(isTouchSupported) { + attachTo.addEventListener('click', (e) => { + //const good = !!findUpClassName(e.target, 'message') || !!findUpClassName(e.target, 'bubble__container'); + const className = (e.target as HTMLElement).className; + const good = ['bubble', 'bubble__container', 'message', 'time', 'inner'].find(c => className.includes(c)); + if(good) { + onContextMenu(e); + } + }); + + attachContextMenuListener(attachTo, (e) => { + if(appImManager.chatSelection.isSelecting) return; + + cancelSelection(); + cancelEvent(e as any); + let bubble = findUpClassName(e.target, 'bubble'); + if(bubble) { + appImManager.chatSelection.toggleByBubble(bubble); + } + }); + } else attachContextMenuListener(attachTo, onContextMenu); } private init = () => { @@ -145,7 +169,7 @@ export default class ChatContextMenu { icon: 'delete danger', text: 'Delete', onClick: this.onDeleteClick, - verify: () => this.peerID > 0 || appMessagesManager.getMessage(this.msgID).fromID == $rootScope.myID || appChatsManager.hasRights(-this.peerID, 'deleteRevoke') + verify: () => appMessagesManager.canDeleteMessage(this.msgID) }]; this.element = ButtonMenu(this.buttons); @@ -217,58 +241,6 @@ export default class ChatContextMenu { }; private onDeleteClick = () => { - const peerID = $rootScope.selectedPeerID; - const firstName = appPeersManager.getPeerTitle(peerID, false, true); - - const msgID = this.msgID; - const callback = (revoke: boolean) => { - appMessagesManager.deleteMessages([msgID], revoke); - }; - - let title: string, description: string, buttons: PopupButton[]; - title = 'Delete Message?'; - description = `Are you sure you want to delete this message?`; - - if(peerID == $rootScope.myID) { - buttons = [{ - text: 'DELETE', - isDanger: true, - callback: () => callback(false) - }]; - } else { - buttons = [{ - text: 'DELETE JUST FOR ME', - isDanger: true, - callback: () => callback(false) - }]; - - if(peerID > 0) { - buttons.push({ - text: 'DELETE FOR ME AND ' + firstName, - isDanger: true, - callback: () => callback(true) - }); - } else if(appChatsManager.hasRights(-peerID, 'deleteRevoke')) { - buttons.push({ - text: 'DELETE FOR ALL', - isDanger: true, - callback: () => callback(true) - }); - } - } - - buttons.push({ - text: 'CANCEL', - isCancel: true - }); - - const popup = new PopupPeer('popup-delete-chat', { - peerID: peerID, - title: title, - description: description, - buttons: buttons - }); - - popup.show(); + new PopupDeleteMessages([this.msgID]); }; } \ No newline at end of file diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 3b763620..3fbdfbd0 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -11,10 +11,11 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; import opusDecodeController from "../../lib/opusDecodeController"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import $rootScope from '../../lib/rootScope'; -import { cancelEvent, getRichValue } from "../../lib/utils"; +import { cancelEvent, findUpClassName, getRichValue } from "../../lib/utils"; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; import PopupCreatePoll from "../popupCreatePoll"; +import PopupForward from '../popupForward'; import PopupNewMedia from '../popupNewMedia'; import { ripple } from '../ripple'; import Scrollable from "../scrollable"; @@ -205,7 +206,7 @@ export class ChatInput { if(this.lastUrl != url) return; //console.log('got webpage: ', webpage); - this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title, webpage.description || webpage.url); + this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title || 'Webpage', webpage.description || webpage.url || ''); delete this.noWebPage; this.willSendWebPage = webpage; @@ -267,7 +268,7 @@ export class ChatInput { //console.log('messageInput paste', text, entities); entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak'); //text = RichTextProcessor.wrapEmojiText(text); - text = RichTextProcessor.wrapRichText(text, {entities}); + text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true}); // console.log('messageInput paste after', text); @@ -453,7 +454,7 @@ export class ChatInput { return; */ - let perf = performance.now(); + //let perf = performance.now(); opusDecodeController.decode(typedArray, true).then(result => { //console.log('WAVEFORM!:', /* waveform, */performance.now() - perf); @@ -509,6 +510,33 @@ export class ChatInput { this.clearHelper(); this.updateSendBtn(); }); + + let d = false; + this.replyElements.container.addEventListener('click', (e) => { + if(!findUpClassName(e.target, 'reply-wrapper')) return; + if(this.helperType == 'forward') { + if(d) return; + d = true; + + const mids = this.forwardingMids.slice(); + const helperFunc = this.helperFunc; + this.clearHelper(); + let selected = false; + new PopupForward(mids, () => { + selected = true; + }, () => { + d = false; + + if(!selected) { + helperFunc(); + } + }); + } else if(this.helperType == 'reply') { + appImManager.setPeer($rootScope.selectedPeerID, this.replyToMsgID); + } else if(this.helperType == 'edit') { + appImManager.setPeer($rootScope.selectedPeerID, this.editMsgID); + } + }); } private isInputEmpty() { @@ -579,8 +607,12 @@ export class ChatInput { }); } + // * wait for sendText set messageID for invokeAfterMsg if(this.forwardingMids.length) { - appMessagesManager.forwardMessages(appImManager.peerID, this.forwardingMids); + const mids = this.forwardingMids.slice(); + setTimeout(() => { + appMessagesManager.forwardMessages(appImManager.peerID, mids); + }, 0); } this.onMessageSent(); @@ -629,7 +661,7 @@ export class ChatInput { this.setTopInfo('forward', f, title, mids.length + ' forwarded messages'); } - this.forwardingMids = mids; + this.forwardingMids = mids.slice(); }; f(); @@ -640,6 +672,12 @@ export class ChatInput { this.messageInput.innerText = ''; } + if(type) { + this.lastUrl = ''; + delete this.noWebPage; + this.willSendWebPage = null; + } + this.replyToMsgID = 0; this.forwardingMids.length = 0; this.editMsgID = 0; @@ -660,9 +698,13 @@ export class ChatInput { } this.chatInput.parentElement.classList.add('is-helper-active'); + /* const scroll = appImManager.scrollable; + if(scroll.isScrolledDown && !scroll.scrollLocked && !appImManager.messagesQueuePromise && !appImManager.setPeerPromise) { + scroll.scrollTo(scroll.scrollHeight, 'top', true, true, 200); + } */ if(input !== undefined) { - this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input) : ''; + this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input, {noLinks: true}) : ''; } setTimeout(() => { diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index f9c06381..33c6bd37 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -1,10 +1,11 @@ import { isTouchSupported } from "../../helpers/touchSupport"; import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; -import { cancelEvent, cancelSelection, findUpClassName } from "../../lib/utils"; +import { cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../lib/utils"; import Button from "../button"; import ButtonIcon from "../buttonIcon"; import CheckboxField from "../checkbox"; +import PopupDeleteMessages from "../popupDeleteMessages"; import PopupForward from "../popupForward"; import { toast } from "../toast"; @@ -34,7 +35,7 @@ const SetTransition = (element: HTMLElement, className: string, forwards: boolea }; const MAX_SELECTION_LENGTH = 100; -const MIN_CLICK_MOVE = 32; // minimum bubble height +//const MIN_CLICK_MOVE = 32; // minimum bubble height export default class ChatSelection { public selectedMids: Set = new Set(); @@ -42,14 +43,25 @@ export default class ChatSelection { private selectionContainer: HTMLElement; private selectionCountEl: HTMLElement; + private selectionForwardBtn: HTMLElement; + private selectionDeleteBtn: HTMLElement; - constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) { - if(isTouchSupported) return; + public selectedText: string; + constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) { const bubblesContainer = appImManager.bubblesContainer; + + if(isTouchSupported) { + bubblesContainer.addEventListener('touchend', (e) => { + if(!this.isSelecting) return; + this.selectedText = getSelectedText(); + }); + return; + } + bubblesContainer.addEventListener('mousedown', (e) => { //console.log('selection mousedown', e); - if(e.button != 0) { // LEFT BUTTON + if(e.button != 0 || (!this.selectedMids.size && !(e.target as HTMLElement).classList.contains('bubble'))) { // LEFT BUTTON return; } @@ -169,6 +181,28 @@ export default class ChatSelection { if(!this.selectedMids.size) return; this.selectionCountEl.innerText = this.selectedMids.size + ' Message' + (this.selectedMids.size == 1 ? '' : 's'); + let cantForward = false, cantDelete = false; + for(const mid of this.selectedMids.values()) { + const message = this.appMessagesManager.getMessage(mid); + if(!cantForward) { + if(message.action) { + cantForward = true; + } + } + + + if(!cantDelete) { + const canDelete = this.appMessagesManager.canDeleteMessage(mid); + if(!canDelete) { + cantDelete = true; + } + } + + if(cantForward && cantDelete) break; + } + + this.selectionForwardBtn.toggleAttribute('disabled', cantForward); + this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete); } public toggleSelection(toggleCheckboxes = true) { @@ -180,11 +214,23 @@ export default class ChatSelection { const bubblesContainer = this.appImManager.bubblesContainer; //bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size); + /* if(bubblesContainer.classList.contains('is-chat-input-hidden')) { + const scrollable = this.appImManager.scrollable; + if(scrollable.isScrolledDown) { + scrollable.scrollTo(scrollable.scrollHeight, 'top', true, true, 200); + } + } */ + SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size, 200, () => { if(!this.isSelecting) { this.selectionContainer.remove(); - this.selectionContainer = null; + this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null; + this.selectedText = undefined; } + + window.requestAnimationFrame(() => { + this.appImManager.onScroll(); + }); }); //const chatInput = this.appImManager.chatInput; @@ -201,19 +247,23 @@ export default class ChatSelection { this.selectionCountEl = document.createElement('div'); this.selectionCountEl.classList.add('selection-container-count'); - const btnForward = Button('btn-primary btn-transparent', {icon: 'forward'}); - btnForward.append('Forward'); - - btnForward.addEventListener('click', () => { + this.selectionForwardBtn = Button('btn-primary btn-transparent selection-container-forward', {icon: 'forward'}); + this.selectionForwardBtn.append('Forward'); + this.selectionForwardBtn.addEventListener('click', () => { new PopupForward([...this.selectedMids], () => { this.cancelSelection(); }); }); - const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete'}); - btnDelete.append('Delete'); + this.selectionDeleteBtn = Button('btn-primary btn-transparent danger selection-container-delete', {icon: 'delete'}); + this.selectionDeleteBtn.append('Delete'); + this.selectionDeleteBtn.addEventListener('click', () => { + new PopupDeleteMessages([...this.selectedMids], () => { + this.cancelSelection(); + }); + }); - this.selectionContainer.append(btnCancel, this.selectionCountEl, btnForward, btnDelete); + this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn); inputMessageDiv.append(this.selectionContainer); } diff --git a/src/components/popup.ts b/src/components/popup.ts index d8829d08..1ccd6cc0 100644 --- a/src/components/popup.ts +++ b/src/components/popup.ts @@ -1,6 +1,5 @@ import $rootScope from "../lib/rootScope"; -import { cancelEvent } from "../lib/utils"; -import AvatarElement from "./avatar"; +import { cancelEvent, findUpClassName } from "../lib/utils"; import { ripple } from "./ripple"; export class PopupElement { @@ -16,7 +15,7 @@ export class PopupElement { protected onCloseAfterTimeout: () => void; protected onEscape: () => boolean = () => true; - constructor(className: string, buttons?: Array, options: Partial<{closable: boolean, withConfirm: string, body: boolean}> = {}) { + constructor(className: string, buttons?: Array, options: Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}> = {}) { this.element.classList.add('popup'); this.element.className = 'popup' + (className ? ' ' + className : ''); this.container.classList.add('popup-container', 'z-depth-1'); @@ -33,6 +32,16 @@ export class PopupElement { this.header.prepend(this.closeBtn); this.closeBtn.addEventListener('click', this.destroy, {once: true}); + + if(options.overlayClosable) { + const onOverlayClick = (e: MouseEvent) => { + if(!findUpClassName(e.target, 'popup-container')) { + this.closeBtn.click(); + } + }; + + this.element.addEventListener('click', onOverlayClick, {once: true}); + } } window.addEventListener('keydown', this._onKeyDown, {capture: true}); diff --git a/src/components/popupDeleteMessages.ts b/src/components/popupDeleteMessages.ts new file mode 100644 index 00000000..f96bcbbf --- /dev/null +++ b/src/components/popupDeleteMessages.ts @@ -0,0 +1,65 @@ +import appChatsManager from "../lib/appManagers/appChatsManager"; +import appMessagesManager from "../lib/appManagers/appMessagesManager"; +import appPeersManager from "../lib/appManagers/appPeersManager"; +import $rootScope from "../lib/rootScope"; +import { PopupButton } from "./popup"; +import PopupPeer from "./popupPeer"; + +export default class PopupDeleteMessages { + constructor(mids: number[], onConfirm?: () => void) { + const peerID = $rootScope.selectedPeerID; + const firstName = appPeersManager.getPeerTitle(peerID, false, true); + + mids = mids.slice(); + const callback = (revoke: boolean) => { + onConfirm && onConfirm(); + appMessagesManager.deleteMessages(mids, revoke); + }; + + let title: string, description: string, buttons: PopupButton[]; + title = `Delete Message${mids.length == 1 ? '' : 's'}?`; + description = `Are you sure you want to delete ${mids.length == 1 ? 'this message' : 'these messages'}?`; + + if(peerID == $rootScope.myID) { + buttons = [{ + text: 'DELETE', + isDanger: true, + callback: () => callback(false) + }]; + } else { + buttons = [{ + text: 'DELETE JUST FOR ME', + isDanger: true, + callback: () => callback(false) + }]; + + if(peerID > 0) { + buttons.push({ + text: 'DELETE FOR ME AND ' + firstName, + isDanger: true, + callback: () => callback(true) + }); + } else if(appChatsManager.hasRights(-peerID, 'deleteRevoke')) { + buttons.push({ + text: 'DELETE FOR ALL', + isDanger: true, + callback: () => callback(true) + }); + } + } + + buttons.push({ + text: 'CANCEL', + isCancel: true + }); + + const popup = new PopupPeer('popup-delete-chat', { + peerID: peerID, + title: title, + description: description, + buttons: buttons + }); + + popup.show(); + } +} \ No newline at end of file diff --git a/src/components/popupForward.ts b/src/components/popupForward.ts index 843b76bb..f4176b91 100644 --- a/src/components/popupForward.ts +++ b/src/components/popupForward.ts @@ -1,3 +1,4 @@ +import { isTouchSupported } from "../helpers/touchSupport"; import appImManager from "../lib/appManagers/appImManager"; import AppSelectPeers from "./appSelectPeers"; import { PopupElement } from "./popup"; @@ -6,8 +7,10 @@ export default class PopupForward extends PopupElement { private selector: AppSelectPeers; //private scrollable: Scrollable; - constructor(mids: number[], onSelect?: () => Promise | void) { - super('popup-forward', null, {closable: true, body: true}); + constructor(mids: number[], onSelect?: () => Promise | void, onClose?: () => void) { + super('popup-forward', null, {closable: true, overlayClosable: true, body: true}); + + if(onClose) this.onClose = onClose; this.selector = new AppSelectPeers(this.body, async() => { const peerID = this.selector.getSelected()[0]; @@ -21,6 +24,10 @@ export default class PopupForward extends PopupElement { appImManager.chatInputC.initMessagesForward(mids.slice()); }, ['dialogs', 'contacts'], () => { this.show(); + + if(!isTouchSupported) { + this.selector.input.focus(); + } }, null, 'send', false); //this.scrollable = new Scrollable(this.body); diff --git a/src/components/popupStickers.ts b/src/components/popupStickers.ts index 187f177c..4de24c4f 100644 --- a/src/components/popupStickers.ts +++ b/src/components/popupStickers.ts @@ -9,6 +9,7 @@ import animationIntersector from "./animationIntersector"; import { findUpClassName } from "../lib/utils"; import appImManager from "../lib/appManagers/appImManager"; import { StickerSet } from "../layer"; +import mediaSizes from "../helpers/mediaSizes"; const ANIMATION_GROUP = 'STICKERS-POPUP'; @@ -24,7 +25,7 @@ export default class PopupStickers extends PopupElement { id: string, access_hash: string }) { - super('popup-stickers', null, {closable: true, body: true}); + super('popup-stickers', null, {closable: true, overlayClosable: true, body: true}); this.h6 = document.createElement('h6'); this.h6.innerText = 'Loading...'; @@ -36,21 +37,12 @@ export default class PopupStickers extends PopupElement { animationIntersector.checkAnimations(false); this.stickersFooter.removeEventListener('click', this.onFooterClick); this.stickersDiv.removeEventListener('click', this.onStickersClick); - this.element.removeEventListener('click', onOverlayClick); }; this.onCloseAfterTimeout = () => { animationIntersector.checkAnimations(undefined, ANIMATION_GROUP); }; - const onOverlayClick = (e: MouseEvent) => { - if(!findUpClassName(e.target, 'popup-container')) { - this.closeBtn.click(); - } - }; - - this.element.addEventListener('click', onOverlayClick); - const div = document.createElement('div'); div.classList.add('sticker-set'); @@ -129,6 +121,8 @@ export default class PopupStickers extends PopupElement { const div = document.createElement('div'); div.classList.add('sticker-set-sticker'); + + const size = mediaSizes.active.esgSticker.width; wrapSticker({ doc, @@ -137,8 +131,8 @@ export default class PopupStickers extends PopupElement { group: ANIMATION_GROUP, play: true, loop: true, - width: 80, - height: 80 + width: size, + height: size }); this.stickersDiv.append(div); diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 6c8c646c..29993529 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -10,7 +10,7 @@ import appMessagesManager from '../lib/appManagers/appMessagesManager'; import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager'; import LottieLoader from '../lib/lottieLoader'; import VideoPlayer from '../lib/mediaPlayer'; -import { formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils"; +import { cancelEvent, formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils"; import webpWorkerController from '../lib/webp/webpWorkerController'; import animationIntersector from './animationIntersector'; import appMediaPlaybackController from './appMediaPlaybackController'; @@ -353,7 +353,7 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals let preloader: ProgressivePreloader; let download: DownloadBlob; - docDiv.addEventListener('click', () => { + docDiv.addEventListener('click', (e) => { if(!download) { if(downloadDiv.classList.contains('downloading')) { return; // means not ready yet diff --git a/src/helpers/touchSupport.ts b/src/helpers/touchSupport.ts index 3521e4ab..f055f004 100644 --- a/src/helpers/touchSupport.ts +++ b/src/helpers/touchSupport.ts @@ -1,2 +1,2 @@ // @ts-ignore -export const isTouchSupported = ('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch); \ No newline at end of file +export const isTouchSupported = ('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch)/* || true */; \ No newline at end of file diff --git a/src/index.hbs b/src/index.hbs index e001c431..e1cf3a2b 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -440,8 +440,10 @@
-
-
+ {{!--
--}} +
+
+ {{!--
--}}