From fd6a39c2bcb8d0d39dbb82baea8b4f46419239b9 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sun, 9 Feb 2020 22:18:04 +0700 Subject: [PATCH] context menu & delete messages & pin message --- src/components/misc.ts | 88 +++++++---- src/components/pageIm.ts | 32 +--- src/components/pageSignUp.ts | 22 +-- src/lib/appManagers/appImManager.ts | 174 +++++++++++++++++++++- src/lib/appManagers/appMessagesManager.ts | 1 - src/lib/lottieLoader.ts | 4 +- src/lib/utils.js | 11 ++ src/scss/partials/_chat.scss | 43 ++++++ src/scss/partials/_chatlist.scss | 7 +- src/scss/style.scss | 22 ++- 10 files changed, 320 insertions(+), 84 deletions(-) diff --git a/src/components/misc.ts b/src/components/misc.ts index 0d401a65..d9f87756 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -410,7 +410,7 @@ export function scrollable(el: HTMLDivElement, x = false, y = true) { let thumbSize = 0; window.addEventListener('resize', resize); //container.addEventListener('DOMNodeInserted', resize); - + let hiddenElements: { up: Element[], down: Element[] @@ -418,23 +418,23 @@ export function scrollable(el: HTMLDivElement, x = false, y = true) { up: [], down: [] }; - + let paddings = {up: 0, down: 0}; - + let paddingTopDiv = document.createElement('div'); paddingTopDiv.classList.add('scroll-padding'); let paddingBottomDiv = document.createElement('div'); paddingBottomDiv.classList.add('scroll-padding'); - + let onScroll = (e: Event) => { // @ts-ignore //let st = container[scrollSide]; - + // @ts-ignore if(container[scrollType] != scrollSize || thumbSize == 0) { resize(); } - + //let splitUp = container.querySelector('ul'); let splitUp = container.children[1]; let children = Array.from(splitUp.children) as HTMLElement[]; @@ -447,57 +447,57 @@ export function scrollable(el: HTMLDivElement, x = false, y = true) { lastVisible = i; } } - + if(firstVisible > 0) { let sliced = children.slice(0, firstVisible); - + for(let child of sliced) { paddings.up += child.scrollHeight; hiddenElements.up.push(child); child.parentElement.removeChild(child); } - + //console.log('sliced up', sliced.length); - + //sliced.forEach(child => child.style.display = 'none'); paddingTopDiv.style.height = paddings.up + 'px'; //console.log('onscroll need to add padding: ', paddings.up); } else if(hiddenElements.up.length) { while(isElementInViewport(paddingTopDiv) && paddings.up) { let child = hiddenElements.up.pop(); - + splitUp.prepend(child); - + paddings.up -= child.scrollHeight; paddingTopDiv.style.height = paddings.up + 'px'; } } - + if(lastVisible < (length - 1)) { let sliced = children.slice(lastVisible + 1).reverse(); - + for(let child of sliced) { paddings.down += child.scrollHeight; hiddenElements.down.unshift(child); child.parentElement.removeChild(child); } - + //console.log('onscroll sliced down', sliced.length); - + //sliced.forEach(child => child.style.display = 'none'); paddingBottomDiv.style.height = paddings.down + 'px'; //console.log('onscroll need to add padding: ', paddings.up); } else if(hiddenElements.down.length) { while(isElementInViewport(paddingBottomDiv) && paddings.down) { let child = hiddenElements.down.shift(); - + splitUp.append(child); - + paddings.down -= child.scrollHeight; paddingBottomDiv.style.height = paddings.down + 'px'; } } - + //console.log('onscroll', container, firstVisible, lastVisible, hiddenElements); // @ts-ignore @@ -508,7 +508,7 @@ export function scrollable(el: HTMLDivElement, x = false, y = true) { // @ts-ignore thumb.style[side] = (value >= maxValue ? maxValue : value) + '%'; - + //lastScrollPos = st; }; @@ -758,15 +758,15 @@ export function getNearestDc() { export function formatPhoneNumber(str: string) { str = str.replace(/\D/g, ''); let phoneCode = str.slice(0, 6); - + console.log('str', str, phoneCode); - + let sortedCountries = Config.Countries.slice().sort((a, b) => b.phoneCode.length - a.phoneCode.length); - + let country = sortedCountries.find((c) => { return c.phoneCode.split(' and ').find((c) => phoneCode.indexOf(c.replace(/\D/g, '')) == 0); }); - + let pattern = country ? country.pattern || country.phoneCode : ''; if(country) { pattern.split('').forEach((symbol, idx) => { @@ -779,6 +779,44 @@ export function formatPhoneNumber(str: string) { str = str.slice(0, country.pattern.length); } } - + return {formatted: str, country}; } + +let onMouseMove = (e: MouseEvent) => { + let rect = openedMenu.getBoundingClientRect(); + let {clientX, clientY} = e; + + let diffX = clientX >= rect.right ? clientX - rect.right : rect.left - clientX; + let diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY; + + if(diffX >= 100 || diffY >= 100) { + openedMenu.classList.remove('active'); + openedMenu.parentElement.classList.remove('menu-open'); + //openedMenu.parentElement.click(); + } + //console.log('mousemove', diffX, diffY); +}; + +let openedMenu: HTMLDivElement = null; +export function openBtnMenu(menuElement: HTMLDivElement) { + if(openedMenu) { + openedMenu.classList.remove('active'); + openedMenu.parentElement.classList.remove('menu-open'); + } + + openedMenu = menuElement; + openedMenu.classList.add('active'); + + window.addEventListener('click', () => { + if(openedMenu) { + openedMenu.parentElement.classList.remove('menu-open'); + openedMenu.classList.remove('active'); + openedMenu = null; + } + + window.removeEventListener('mousemove', onMouseMove); + }, {once: true}); + + window.addEventListener('mousemove', onMouseMove); +} diff --git a/src/components/pageIm.ts b/src/components/pageIm.ts index 490581fe..df5321af 100644 --- a/src/components/pageIm.ts +++ b/src/components/pageIm.ts @@ -1,5 +1,5 @@ //import { appImManager, appMessagesManager, appDialogsManager, apiUpdatesManager, appUsersManager } from "../lib/services"; -import { horizontalMenu, wrapSticker, MTDocument, LazyLoadQueue } from "./misc"; +import { horizontalMenu, wrapSticker, MTDocument, LazyLoadQueue, openBtnMenu } from "./misc"; import Scrollable from './scrollable'; import { whichChild, findUpTag } from "../lib/utils"; @@ -712,27 +712,13 @@ export default () => import('../lib/services').then(services => { toggleEmoticons.classList.toggle('active'); }; */ - let openedMenu: HTMLDivElement = null; - let onMouseMove = (e: MouseEvent) => { - let rect = openedMenu.getBoundingClientRect(); - let {clientX, clientY} = e; - - let diffX = clientX >= rect.right ? clientX - rect.right : rect.left - clientX; - let diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY; - - if(diffX >= 100 || diffY >= 100) { - openedMenu.parentElement.click(); - } - //console.log('mousemove', diffX, diffY); - }; - Array.from(document.getElementsByClassName('btn-menu-toggle')).forEach((el) => { el.addEventListener('click', (e) => { console.log('click pageIm'); if(!el.classList.contains('btn-menu-toggle')) return false; - window.removeEventListener('mousemove', onMouseMove); - openedMenu = el.querySelector('.btn-menu'); + //window.removeEventListener('mousemove', onMouseMove); + let openedMenu = el.querySelector('.btn-menu') as HTMLDivElement; e.cancelBubble = true; if(el.classList.contains('menu-open')) { @@ -740,16 +726,8 @@ export default () => import('../lib/services').then(services => { openedMenu.classList.remove('active'); } else { el.classList.add('menu-open'); - openedMenu.classList.add('active'); - - window.addEventListener('click', () => { - //(el as HTMLDivElement).click(); - el.classList.remove('menu-open'); - openedMenu.classList.remove('active'); - window.removeEventListener('mousemove', onMouseMove); - }, {once: true}); - - window.addEventListener('mousemove', onMouseMove); + + openBtnMenu(openedMenu); } }); }); diff --git a/src/components/pageSignUp.ts b/src/components/pageSignUp.ts index 1b002bb9..4933c2b7 100644 --- a/src/components/pageSignUp.ts +++ b/src/components/pageSignUp.ts @@ -18,22 +18,6 @@ export default (_authCode: typeof authCode) => { let pageElement = document.body.getElementsByClassName('page-signUp')[0] as HTMLDivElement; pageElement.style.display = ''; - let findUpClassName = (el: Element | HTMLElement, className: string): HTMLElement => { - while(el.parentNode) { - // @ts-ignore - el = el.parentNode; - if(el.classList.contains(className)) - return el; - } - return null; - }; - Array.from(document.body.getElementsByClassName('popup-close')).forEach(el => { - let popup = findUpClassName(el, 'popup'); - el.addEventListener('click', () => { - popup.classList.remove('is-visible'); - }); - }); - const avatarInput = document.getElementById('avatar-input') as HTMLInputElement; const avatarPopup = pageElement.getElementsByClassName('popup-avatar')[0]; const avatarPreview = pageElement.querySelector('#canvas-avatar') as HTMLCanvasElement; @@ -46,7 +30,7 @@ export default (_authCode: typeof authCode) => { (avatarPopup.getElementsByClassName('popup-close')[0] as HTMLButtonElement) .addEventListener('click', function(this, e) { /* let popup = findUpClassName(this, 'popup'); - popup.classList.remove('is-visible'); */ + popup.classList.remove('active'); */ setTimeout(() => { cropper.removeHandlers(); @@ -67,7 +51,7 @@ export default (_authCode: typeof authCode) => { // apply avatarPopup.getElementsByClassName('btn-crop')[0].addEventListener('click', () => { cropper.crop(); - avatarPopup.classList.remove('is-visible'); + avatarPopup.classList.remove('active'); cropper.removeHandlers(); avatarPreview.toBlob(blob => { @@ -120,7 +104,7 @@ export default (_authCode: typeof authCode) => { avatarInput.value = ''; }; - avatarPopup.classList.add('is-visible'); + avatarPopup.classList.add('active'); //console.log(contents); }; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 4a2b5f96..5797b906 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -1,10 +1,10 @@ import apiManager from '../mtproto/apiManager'; -import { $rootScope, isElementInViewport, numberWithCommas } from "../utils"; +import { $rootScope, isElementInViewport, numberWithCommas, findUpClassName } from "../utils"; import appUsersManager from "./appUsersManager"; import appMessagesManager from "./appMessagesManager"; import appPeersManager from "./appPeersManager"; import appProfileManager from "./appProfileManager"; -import { ProgressivePreloader, wrapDocument, wrapSticker, wrapVideo, wrapPhoto } from "../../components/misc"; +import { ProgressivePreloader, wrapDocument, wrapSticker, wrapVideo, wrapPhoto, openBtnMenu } from "../../components/misc"; import appDialogsManager from "./appDialogsManager"; import { RichTextProcessor } from "../richtextprocessor"; import appPhotosManager from "./appPhotosManager"; @@ -16,6 +16,7 @@ import appMediaViewer from "./appMediaViewer"; import appSidebarLeft from "./appSidebarLeft"; import appChatsManager from "./appChatsManager"; import appMessagesIDsManager from "./appMessagesIDsManager"; +import apiUpdatesManager from './apiUpdatesManager'; console.log('appImManager included!'); @@ -133,13 +134,29 @@ export class AppImManager { private topbar: HTMLDivElement = null; private chatInput: HTMLDivElement = null; - scrolledAll: boolean; + private scrolledAll: boolean; + + public contextMenu = document.getElementById('bubble-contextmenu') as HTMLDivElement; + private contextMenuPin = this.contextMenu.querySelector('.menu-pin') as HTMLDivElement; + private contextMenuMsgID: number; + + private popupDeleteMessage: { + popupEl?: HTMLDivElement, + deleteBothBtn?: HTMLButtonElement, + deleteMeBtn?: HTMLButtonElement, + cancelBtn?: HTMLButtonElement + } = {}; constructor() { this.log = logger('IM'); this.preloader = new ProgressivePreloader(null, false); + this.popupDeleteMessage.popupEl = this.pageEl.querySelector('.popup-delete-message') as HTMLDivElement; + this.popupDeleteMessage.deleteBothBtn = this.popupDeleteMessage.popupEl.querySelector('.popup-delete-both') as HTMLButtonElement; + this.popupDeleteMessage.deleteMeBtn = this.popupDeleteMessage.popupEl.querySelector('.popup-delete-me') as HTMLButtonElement; + this.popupDeleteMessage.cancelBtn = this.popupDeleteMessage.popupEl.querySelector('.popup-close') as HTMLButtonElement; + apiManager.getUserID().then((id) => { this.myID = id; }); @@ -256,6 +273,111 @@ export class AppImManager { this.btnMenuMute.addEventListener('click', () => this.mutePeer()); this.btnMute.addEventListener('click', () => this.mutePeer()); + this.chatInner.addEventListener('contextmenu', e => { + let bubble = findUpClassName(e.target, 'bubble'); + if(bubble) { + e.preventDefault(); + e.cancelBubble = true; + + let msgID = 0; + for(let id in this.bubbles) { + if(this.bubbles[id] === bubble) { + msgID = +id; + break; + } + } + + if(!msgID) return; + + if(this.myID == this.peerID || + (this.peerID < 0 && !appPeersManager.isChannel(this.peerID) && !appPeersManager.isMegagroup(this.peerID))) { + this.contextMenuPin.style.display = ''; + } else this.contextMenuPin.style.display = 'none'; + + this.contextMenuMsgID = msgID; + + let side = bubble.parentElement.classList.contains('in') ? 'left' : 'right'; + + this.contextMenu.classList.remove('bottom-left', 'bottom-right'); + this.contextMenu.classList.add(side == 'left' ? 'bottom-right' : 'bottom-left'); + + let {clientX, clientY} = e; + + this.contextMenu.style.left = (side == 'right' ? clientX - this.contextMenu.scrollWidth : clientX) + 'px'; + if((clientY + this.contextMenu.scrollHeight) > window.innerHeight) { + this.contextMenu.style.top = (window.innerHeight - this.contextMenu.scrollHeight) + 'px'; + } else { + this.contextMenu.style.top = clientY + 'px'; + } + + //this.contextMenu.classList.add('active'); + openBtnMenu(this.contextMenu); + + this.log('contextmenu', e, bubble, msgID, side); + } + }); + + this.contextMenu.querySelector('.menu-copy').addEventListener('click', () => { + let message = appMessagesManager.getMessage(this.contextMenuMsgID); + + let str = message ? message.message : ''; + + var textArea = document.createElement("textarea"); + textArea.value = str; + textArea.style.position = "fixed"; //avoid scrolling to bottom + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + } catch (err) { + console.error('Oops, unable to copy', err); + } + + document.body.removeChild(textArea); + }); + + this.contextMenu.querySelector('.menu-delete').addEventListener('click', () => { + if(this.peerID == this.myID) { + this.popupDeleteMessage.deleteBothBtn.style.display = 'none'; + this.popupDeleteMessage.deleteMeBtn.innerText = 'DELETE'; + } else { + this.popupDeleteMessage.deleteBothBtn.style.display = ''; + this.popupDeleteMessage.deleteMeBtn.innerText = 'DELETE JUST FOR ME'; + + if(this.peerID > 0) { + let title = appPeersManager.getPeerTitle(this.peerID); + this.popupDeleteMessage.deleteBothBtn.innerText = 'DELETE FOR ME AND ' + title.split(' ')[0]; + } else { + this.popupDeleteMessage.deleteBothBtn.innerText = 'DELETE FOR ALL'; + } + } + + this.popupDeleteMessage.popupEl.classList.add('active'); + }); + + this.contextMenuPin.addEventListener('click', () => { + apiManager.invokeApi('messages.updatePinnedMessage', { + flags: 0, + peer: appPeersManager.getInputPeerByID(this.peerID), + id: this.contextMenuMsgID + }).then(updates => { + this.log('pinned updates:', updates); + apiUpdatesManager.processUpdateMessage(updates); + }); + }); + + this.popupDeleteMessage.deleteBothBtn.addEventListener('click', () => { + this.deleteMessages(true); + this.popupDeleteMessage.cancelBtn.click(); + }); + + this.popupDeleteMessage.deleteMeBtn.addEventListener('click', () => { + this.deleteMessages(false); + this.popupDeleteMessage.cancelBtn.click(); + }); + this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3); this.updateStatus(); setInterval(() => this.setPeerStatus(), 60e3); @@ -263,6 +385,36 @@ export class AppImManager { this.loadMediaQueueProcess(); } + public deleteMessages(revoke = false) { + let flags = revoke ? 1 : 0; + let ids = [this.contextMenuMsgID]; + + apiManager.invokeApi('messages.deleteMessages', { + flags: flags, + revoke: revoke, + id: ids + }).then((affectedMessages: any) => { + this.log('deleted messages:', affectedMessages); + + apiUpdatesManager.processUpdateMessage({ + _: 'updateShort', + update: { + _: 'updatePts', + pts: affectedMessages.pts, + pts_count: affectedMessages.pts_count + } + }); + + apiUpdatesManager.processUpdateMessage({ + _: 'updateShort', + update: { + _: 'updateDeleteMessages', + messages: ids + } + }); + }); + } + public loadMediaQueuePush(cb: () => Promise) { this.loadMediaQueue.push(cb); this.loadMediaQueueProcess(); @@ -1344,6 +1496,22 @@ export class AppImManager { this.log('updateNotifySettings', peerID, notify_settings); break; } + + case 'updateChatPinnedMessage': + case 'updateUserPinnedMessage': { + let {id} = update; + + this.log('updateUserPinnedMessage', update); + + this.pinnedMsgID = id; + // hz nado li tut appMessagesIDsManager.getFullMessageID(update.max_id, channelID); + let peerID = update.user_id || -update.chat_id || -update.channel_id; + if(peerID == this.peerID) { + appMessagesManager.wrapSingleMessage(id); + } + + break; + } } } } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index dd1c04f8..6fec01e7 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -7,7 +7,6 @@ import { nextRandomInt, bigint } from "../bin_utils"; import { MTProto, telegramMeWebService } from "../mtproto/mtproto"; import apiUpdatesManager from "./apiUpdatesManager"; import appPhotosManager from "./appPhotosManager"; -import appProfileManager from "./appProfileManager"; import AppStorage from '../storage'; import AppPeersManager from "./appPeersManager"; diff --git a/src/lib/lottieLoader.ts b/src/lib/lottieLoader.ts index 88b19add..06042cf6 100644 --- a/src/lib/lottieLoader.ts +++ b/src/lib/lottieLoader.ts @@ -33,7 +33,7 @@ class LottieLoader { if(canvas && isElementInViewport(container)) { let c = container.firstElementChild as HTMLCanvasElement; if(!c.height && !c.width) { - console.log('lottie need resize'); + //console.log('lottie need resize'); animation.resize(); } } @@ -104,7 +104,7 @@ class LottieLoader { public getAnimation(el: HTMLElement, group = '') { let groups = group ? [group] : Object.keys(this.animations); - console.log('getAnimation', groups, this.animations); + //console.log('getAnimation', groups, this.animations); for(let group of groups) { let animations = this.animations[group]; diff --git a/src/lib/utils.js b/src/lib/utils.js index 2d09d971..9951304d 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -324,6 +324,17 @@ export function numberWithCommas(x) { return parts.join("."); } +export function findUpClassName(el, className) { + if(el.classList.contains(className)) return el; // 03.02.2020 + + while(el.parentNode) { + el = el.parentNode; + if(el.classList.contains(className)) + return el; + } + return null; +} + export function findUpTag(el, tag) { if(el.tagName == tag) return el; // 03.02.2020 diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 5b83f91f..a7ba9fc7 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -883,6 +883,12 @@ } */ } +#bubble-contextmenu { + position: fixed; + right: auto; + bottom: auto; +} + .emoji-dropdown { position: absolute; left: 0; @@ -1088,3 +1094,40 @@ } } } + +.popup { + &.popup-delete-message { + + + .popup-header { + margin-bottom: 1rem; + } + } + + .popup-buttons { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + + button { + background: none; + outline: none; + border: none; + padding: .5rem .5rem; + text-transform: uppercase; + transition: .2s; + border-radius: $border-radius; + cursor: pointer; + color: $blue; + + &:hover { + background-color: rgba(112, 117, 121, 0.08); + } + + & + button { + margin-top: .5rem; + } + } + } +} diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 2bd4c8fa..16e3c330 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -64,7 +64,7 @@ position: relative; cursor: pointer; padding: 0 8.5px; - margin: 0 8.5px; + margin: 0 8.5px 0 8px; overflow: hidden; &:hover { @@ -146,11 +146,12 @@ } .message-status { - margin-right: .25rem; + margin-right: .1rem; + margin-top: .3rem; &[class*=" tgico-"] { color: $success-color; - font-size: 1.15rem; + font-size: 1.25rem; } } diff --git a/src/scss/style.scss b/src/scss/style.scss index f1e1cea2..fae8c67b 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -175,6 +175,20 @@ input { transform-origin: top left; } + &.top-left { + top: initial; + right: 0; + bottom: 100%; + transform-origin: bottom right; + } + + &.top-right { + top: initial; + left: 0; + bottom: 100%; + transform-origin: bottom left; + } + > div { display: flex; position: relative; @@ -925,7 +939,7 @@ $width: 100px; } .popup { - position: fixed; + position: fixed!important; left: 0; top: 0; height: 100%; @@ -948,7 +962,7 @@ $width: 100px; justify-content: center; } -.popup.is-visible { +.popup.active { opacity: 1; visibility: visible; -webkit-transition: opacity 0.3s 0s, visibility 0s 0s; @@ -977,7 +991,7 @@ $width: 100px; transition-duration: 0.3s; } -.popup-close { +span.popup-close { /* position: absolute; left: 20px; top: 12.5px; */ @@ -1021,7 +1035,7 @@ $width: 100px; } */ } -.popup.is-visible .popup-container { +.popup.active .popup-container { -webkit-transform: translateY(0); -moz-transform: translateY(0); -ms-transform: translateY(0);