From 984b04ab40db5fc2d16e03dad678414bceddea31 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sat, 21 Nov 2020 15:13:23 +0200 Subject: [PATCH] Fix context menu again Multiselect fixes for desktop & mobile Safari Fix multiselect album on mobiles --- src/components/appMediaViewer.ts | 6 +-- src/components/audio.ts | 24 ++++++---- src/components/buttonMenu.ts | 5 +- src/components/buttonMenuToggle.ts | 6 +-- src/components/chat/contextMenu.ts | 41 +++++++++++++--- src/components/chat/selection.ts | 4 +- src/components/misc.ts | 38 ++++++++------- src/components/poll.ts | 18 +++---- src/components/popup.ts | 3 +- src/components/sidebarLeft/index.ts | 14 +++--- src/components/wrappers.ts | 8 ++-- src/helpers/dom.ts | 52 +++++++++++++++++++- src/lib/appManagers/apiUpdatesManager.ts | 2 +- src/lib/appManagers/appDialogsManager.ts | 60 +++++++++++++----------- src/lib/appManagers/appImManager.ts | 29 ++++++++---- src/scss/components/_global.scss | 2 +- src/scss/partials/_avatar.scss | 1 + src/scss/partials/_button.scss | 13 +++++ src/scss/partials/_chatBubble.scss | 13 ++++- src/scss/partials/_input.scss | 2 +- src/scss/partials/_leftSidebar.scss | 15 ++---- src/scss/style.scss | 35 +++++++++++++- 22 files changed, 278 insertions(+), 113 deletions(-) diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index fcceff31..1a59ea79 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -1173,7 +1173,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet this.openMedia(appMessagesManager.getMessage(target.mid), target.element); }; - onForwardClick = (e: MouseEvent) => { + onForwardClick = () => { if(this.currentMessageID) { //appSidebarRight.forwardTab.open([this.currentMessageID]); new PopupForward([this.currentMessageID], () => { @@ -1198,7 +1198,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet } }; - onDownloadClick = (e: MouseEvent) => { + onDownloadClick = () => { const message = appMessagesManager.getMessage(this.currentMessageID); if(message.media.photo) { appPhotosManager.savePhotoFile(message.media.photo); @@ -1370,7 +1370,7 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe this.openMedia(target.photoID, target.element, 1); }; - onDownloadClick = (e: MouseEvent) => { + onDownloadClick = () => { appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.currentPhotoID)); }; diff --git a/src/components/audio.ts b/src/components/audio.ts index 345c944e..e7f03795 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -11,6 +11,7 @@ import { isSafari } from "../helpers/userAgent"; import appMessagesManager from "../lib/appManagers/appMessagesManager"; import rootScope from "../lib/rootScope"; import './middleEllipsis'; +import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom"; rootScope.on('messages_media_read', e => { const mids = e.detail; @@ -213,12 +214,14 @@ function wrapVoiceMessage(doc: MyDocument, audioEl: AudioElement, mid: number) { mousedown = false; } }); - progress.addEventListener('click', (e) => { + progress.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); if(!audio.paused) scrub(e); }); - function scrub(e: MouseEvent) { - const scrubTime = e.offsetX / availW /* width */ * audio.duration; + function scrub(e: MouseEvent | TouchEvent) { + const offsetX = e instanceof MouseEvent ? e.offsetX : e.changedTouches[0].clientX; + const scrubTime = offsetX / availW /* width */ * audio.duration; lastIndex = Math.round(scrubTime / audio.duration * barCount); rects.slice(0, lastIndex + 1).forEach(node => node.classList.add('active')); @@ -366,7 +369,8 @@ export default class AudioElement extends HTMLElement { audioTimeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true) + ' / ' + durationStr; } - toggle.addEventListener('click', () => { + toggle.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); if(audio.paused) audio.play().catch(() => {}); else audio.pause(); }); @@ -395,7 +399,8 @@ export default class AudioElement extends HTMLElement { if(doc.type == 'voice') { let download: Download; - const onClick = () => { + const onClick = (e: Event) => { + cancelEvent(e); if(!download) { if(!preloader) { preloader = new ProgressivePreloader(null, true); @@ -406,7 +411,7 @@ export default class AudioElement extends HTMLElement { download.then(() => { downloadDiv.remove(); - this.removeEventListener('click', onClick); + this.removeEventListener(CLICK_EVENT_NAME, onClick); onLoad(); }).catch(err => { if(err.name === 'AbortError') { @@ -422,7 +427,7 @@ export default class AudioElement extends HTMLElement { } }; - this.addEventListener('click', onClick); + this.addEventListener(CLICK_EVENT_NAME, onClick); this.click(); } else { onLoad(false); @@ -430,8 +435,9 @@ export default class AudioElement extends HTMLElement { //if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано //onLoad(); //} else { - const r = () => { + const r = (e: Event) => { //onLoad(); + cancelEvent(e); appMediaPlaybackController.resolveWaitingForLoadMedia(mid); appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio @@ -464,7 +470,7 @@ export default class AudioElement extends HTMLElement { }); }; - this.addEventListener('click', r, {once: true}); + this.addEventListener(CLICK_EVENT_NAME, r, {once: true}); //} } } else { diff --git a/src/components/buttonMenu.ts b/src/components/buttonMenu.ts index 5c6ddc81..cc157b05 100644 --- a/src/components/buttonMenu.ts +++ b/src/components/buttonMenu.ts @@ -1,6 +1,7 @@ +import { CLICK_EVENT_NAME } from "../helpers/dom"; import { ripple } from "./ripple"; -export type ButtonMenuItemOptions = {icon: string, text: string, onClick: (e: MouseEvent) => void, element?: HTMLElement}; +export type ButtonMenuItemOptions = {icon: string, text: string, onClick: (e: MouseEvent | TouchEvent) => void, element?: HTMLElement}; const ButtonMenuItem = (options: ButtonMenuItemOptions) => { if(options.element) return options.element; @@ -11,7 +12,7 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => { el.innerText = text; ripple(el); - el.addEventListener('click', onClick); + el.addEventListener(CLICK_EVENT_NAME, onClick); return options.element = el; }; diff --git a/src/components/buttonMenuToggle.ts b/src/components/buttonMenuToggle.ts index d92c7c6e..d4727a7a 100644 --- a/src/components/buttonMenuToggle.ts +++ b/src/components/buttonMenuToggle.ts @@ -1,3 +1,4 @@ +import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom"; import ButtonIcon from "./buttonIcon"; import ButtonMenu, { ButtonMenuItemOptions } from "./buttonMenu"; import { closeBtnMenu, openBtnMenu } from "./misc"; @@ -12,14 +13,13 @@ const ButtonMenuToggle = (options: Partial<{noRipple: true, onlyMobile: true}> = }; const ButtonMenuToggleHandler = (el: HTMLElement) => { - (el as HTMLElement).addEventListener('click', (e) => { + (el as HTMLElement).addEventListener(CLICK_EVENT_NAME, (e) => { //console.log('click pageIm'); if(!el.classList.contains('btn-menu-toggle')) return false; //window.removeEventListener('mousemove', onMouseMove); const openedMenu = el.querySelector('.btn-menu') as HTMLDivElement; - e.cancelBubble = true; - //cancelEvent(e); + cancelEvent(e); if(el.classList.contains('menu-open')) { closeBtnMenu(); diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 2142e6df..8d07b0f0 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -87,33 +87,60 @@ export default class ChatContextMenu { const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right'; //bubble.parentElement.append(this.element); + //appImManager.log('contextmenu', e, bubble, side); positionMenu(e, this.element, side); openBtnMenu(this.element, () => { this.peerID = this.msgID = 0; this.target = null; }); - - /////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 attachClickEvent = (elem: HTMLElement, callback: (e: TouchEvent) => void) => { + elem.addEventListener('touchstart', (e) => { + const onTouchMove = (e: TouchEvent) => { + elem.removeEventListener('touchend', onTouchEnd); + }; + + const onTouchEnd = (e: TouchEvent) => { + elem.removeEventListener('touchmove', onTouchMove); + callback(e); + }; + + elem.addEventListener('touchend', onTouchEnd, {once: true}); + elem.addEventListener('touchmove', onTouchMove, {once: true}); + }); + }; + + attachClickEvent(attachTo, (e) => { + if(appImManager.chatSelection.isSelecting) { + return; + } + const className = (e.target as HTMLElement).className; if(!className || !className.includes) return; + appImManager.log('touchend', e); + const good = ['bubble', 'bubble__container', 'message', 'time', 'inner'].find(c => className.match(new RegExp(c + '($|\\s)'))); if(good) { - onContextMenu(e); + cancelEvent(e); + onContextMenu(e.changedTouches[0]); } }); attachContextMenuListener(attachTo, (e) => { if(appImManager.chatSelection.isSelecting) return; + // * these two lines will fix instant text selection on iOS Safari + attachTo.classList.add('no-select'); + attachTo.addEventListener('touchend', () => { + attachTo.classList.remove('no-select'); + }, {once: true}); + cancelSelection(); - cancelEvent(e as any); - let bubble = findUpClassName(e.target, 'bubble'); + //cancelEvent(e as any); + const bubble = findUpClassName(e.target, 'album-item') || findUpClassName(e.target, 'bubble'); if(bubble) { appImManager.chatSelection.toggleByBubble(bubble); } diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index d7b18e06..6fefe25d 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -1,7 +1,7 @@ import { isTouchSupported } from "../../helpers/touchSupport"; import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; -import { cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom"; +import { blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom"; import Button from "../button"; import ButtonIcon from "../buttonIcon"; import CheckboxField from "../checkbox"; @@ -225,6 +225,8 @@ export default class ChatSelection { } } + blurActiveElement(); // * for mobile keyboards + SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size, 200, () => { if(!this.isSelecting) { this.selectionContainer.remove(); diff --git a/src/components/misc.ts b/src/components/misc.ts index b1a79422..52e3750c 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -1,5 +1,5 @@ import Countries, { Country, PhoneCodesMain } from "../countries"; -import { cancelEvent } from "../helpers/dom"; +import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom"; import mediaSizes from "../helpers/mediaSizes"; import { clamp } from "../helpers/number"; import { isTouchSupported } from "../helpers/touchSupport"; @@ -148,7 +148,7 @@ export const closeBtnMenu = () => { window.removeEventListener('contextmenu', onClick); } - document.removeEventListener('click', onClick); + document.removeEventListener(CLICK_EVENT_NAME, onClick); }; window.addEventListener('resize', () => { @@ -186,18 +186,21 @@ export function openBtnMenu(menuElement: HTMLElement, onClose?: () => void) { } // ! safari iOS doesn't handle window click event on overlay, idk why - document.addEventListener('click', onClick); + document.addEventListener(CLICK_EVENT_NAME, onClick); } const PADDING_TOP = 8; const PADDING_LEFT = 8; -export function positionMenu({clientX, clientY}: {clientX: number, clientY: number}/* e: MouseEvent */, elem: HTMLElement, side?: 'left' | 'right' | 'center') { +export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLElement, side?: 'left' | 'right' | 'center') { //let {clientX, clientY} = e; // * side mean the OPEN side let {scrollWidth: menuWidth, scrollHeight: menuHeight} = elem; - let {innerWidth: windowWidth, innerHeight: windowHeight} = window; + //let {innerWidth: windowWidth, innerHeight: windowHeight} = window; + const rect = document.body.getBoundingClientRect(); + const windowWidth = rect.width; + const windowHeight = rect.height; side = mediaSizes.isMobile ? 'right' : 'left'; let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top'; @@ -205,17 +208,17 @@ export function positionMenu({clientX, clientY}: {clientX: number, clientY: numb const getSides = () => { return { x: { - left: clientX, - right: clientX - menuWidth + left: pageX, + right: pageX - menuWidth }, intermediateX: side == 'right' ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, //intermediateX: clientX < windowWidth / 2 ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, y: { - top: clientY, - bottom: clientY - menuHeight + top: pageY, + bottom: pageY - menuHeight }, //intermediateY: verticalSide == 'top' ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, - intermediateY: clientY < windowHeight / 2 ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, + intermediateY: pageY < windowHeight / 2 ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, }; }; @@ -290,11 +293,13 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To if(isApple && isTouchSupported) { let timeout: number; + const options: any = /* null */{capture: true}; + const onCancel = () => { clearTimeout(timeout); - element.removeEventListener('touchmove', onCancel); - element.removeEventListener('touchend', onCancel); - element.removeEventListener('touchcancel', onCancel); + element.removeEventListener('touchmove', onCancel, options); + element.removeEventListener('touchend', onCancel, options); + element.removeEventListener('touchcancel', onCancel, options); }; element.addEventListener('touchstart', (e) => { @@ -303,11 +308,12 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To return; } - element.addEventListener('touchmove', onCancel); - element.addEventListener('touchend', onCancel); - element.addEventListener('touchcancel', onCancel); + element.addEventListener('touchmove', onCancel, options); + element.addEventListener('touchend', onCancel, options); + element.addEventListener('touchcancel', onCancel, options); timeout = window.setTimeout(() => { + element.addEventListener('touchend', cancelEvent, {once: true}); // * fix instant closing callback(e.touches[0]); onCancel(); }, .4e3); diff --git a/src/components/poll.ts b/src/components/poll.ts index df8bf20b..a19d78e1 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -5,7 +5,7 @@ import appPollsManager, { Poll, PollResults } from "../lib/appManagers/appPollsM import serverTimeManager from "../lib/mtproto/serverTimeManager"; import { RichTextProcessor } from "../lib/richtextprocessor"; import rootScope from "../lib/rootScope"; -import { cancelEvent, findUpClassName } from "../helpers/dom"; +import { cancelEvent, CLICK_EVENT_NAME, findUpClassName } from "../helpers/dom"; import { ripple } from "./ripple"; import appSidebarRight from "./sidebarRight"; @@ -338,7 +338,8 @@ export default class PollElement extends HTMLElement { this.votersCountDiv.classList.add('hide'); } - this.sendVoteBtn.addEventListener('click', () => { + this.sendVoteBtn.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); /* const indexes = this.answerDivs.filter(el => el.classList.contains('is-chosing')).map(el => +el.dataset.index); if(indexes.length) { @@ -363,7 +364,7 @@ export default class PollElement extends HTMLElement { this.performResults(results, poll.chosenIndexes); } else if(!this.isClosed) { this.setVotersCount(results); - this.addEventListener('click', this.clickHandler); + this.addEventListener(CLICK_EVENT_NAME, this.clickHandler); } } @@ -405,7 +406,7 @@ export default class PollElement extends HTMLElement { this.descDiv.append(toggleHint); //let active = false; - toggleHint.addEventListener('click', (e) => { + toggleHint.addEventListener(CLICK_EVENT_NAME, (e) => { cancelEvent(e); //active = true; @@ -425,12 +426,13 @@ export default class PollElement extends HTMLElement { } } - clickHandler(e: MouseEvent) { + clickHandler(e: Event) { const target = findUpClassName(e.target, 'poll-answer') as HTMLElement; if(!target) { return; } - + + cancelEvent(e); const answerIndex = +target.dataset.index; if(this.isMultiple) { target.classList.toggle('is-chosing'); @@ -512,9 +514,9 @@ export default class PollElement extends HTMLElement { this.chosenIndexes = chosenIndexes.slice(); if(this.isRetracted) { - this.addEventListener('click', this.clickHandler); + this.addEventListener(CLICK_EVENT_NAME, this.clickHandler); } else { - this.removeEventListener('click', this.clickHandler); + this.removeEventListener(CLICK_EVENT_NAME, this.clickHandler); } } diff --git a/src/components/popup.ts b/src/components/popup.ts index 768b45eb..0156cb6d 100644 --- a/src/components/popup.ts +++ b/src/components/popup.ts @@ -1,5 +1,5 @@ import rootScope from "../lib/rootScope"; -import { cancelEvent, findUpClassName } from "../helpers/dom"; +import { blurActiveElement, cancelEvent, findUpClassName } from "../helpers/dom"; import { ripple } from "./ripple"; export class PopupElement { @@ -100,6 +100,7 @@ export class PopupElement { }; public show() { + blurActiveElement(); // * hide mobile keyboard document.body.append(this.element); void this.element.offsetWidth; // reflow this.element.classList.add('active'); diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 856a4fea..9a042dbd 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -8,7 +8,7 @@ import appStateManager from "../../lib/appManagers/appStateManager"; import appUsersManager from "../../lib/appManagers/appUsersManager"; import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config"; import rootScope from "../../lib/rootScope"; -import { findUpClassName, findUpTag } from "../../helpers/dom"; +import { CLICK_EVENT_NAME, findUpClassName, findUpTag } from "../../helpers/dom"; import AppSearch, { SearchGroup } from "../appSearch"; import "../avatar"; import { parseMenuButtonsTo, putPreloader } from "../misc"; @@ -292,32 +292,32 @@ export class AppSidebarLeft extends SidebarSlider { this.archivedCount = this.buttons.archived.querySelector('.archived-count') as HTMLSpanElement; - this.buttons.saved.addEventListener('click', (e) => { + this.buttons.saved.addEventListener(CLICK_EVENT_NAME, (e) => { ///////this.log('savedbtn click'); setTimeout(() => { // menu doesn't close if no timeout (lol) appImManager.setPeer(appImManager.myID); }, 0); }); - this.buttons.archived.addEventListener('click', (e) => { + this.buttons.archived.addEventListener(CLICK_EVENT_NAME, (e) => { this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.archived); }); - this.buttons.contacts.addEventListener('click', (e) => { + this.buttons.contacts.addEventListener(CLICK_EVENT_NAME, (e) => { this.contactsTab.openContacts(); }); - this.buttons.settings.addEventListener('click', (e) => { + this.buttons.settings.addEventListener(CLICK_EVENT_NAME, (e) => { this.settingsTab.fillElements(); this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.settings); }); - this.newButtons.channel.addEventListener('click', (e) => { + this.newButtons.channel.addEventListener(CLICK_EVENT_NAME, (e) => { this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.newChannel); }); [this.newButtons.group, this.buttons.newGroup].forEach(btn => { - btn.addEventListener('click', (e) => { + btn.addEventListener(CLICK_EVENT_NAME, (e) => { this.addMembersTab.init(0, 'chat', false, (peerIDs) => { this.newGroupTab.init(peerIDs); }); diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 44af37c8..d67349d8 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -12,7 +12,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 { isInDOM } from "../helpers/dom"; +import { cancelEvent, CLICK_EVENT_NAME, isInDOM } from "../helpers/dom"; import webpWorkerController from '../lib/webp/webpWorkerController'; import animationIntersector from './animationIntersector'; import appMediaPlaybackController from './appMediaPlaybackController'; @@ -373,7 +373,8 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals let preloader: ProgressivePreloader; let download: DownloadBlob; - docDiv.addEventListener('click', (e) => { + docDiv.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); if(!download) { if(downloadDiv.classList.contains('downloading')) { return; // means not ready yet @@ -670,7 +671,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o }, true); if(emoji) { - div.addEventListener('click', () => { + div.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); let animation = LottieLoader.getAnimation(div); if(animation.paused) { diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 8f1988df..c4d4c29a 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -1,4 +1,5 @@ import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; +import { isTouchSupported } from "./touchSupport"; /* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { if(!element) { @@ -66,6 +67,38 @@ export function placeCaretAtEnd(el: HTMLElement) { } } +/* export function getFieldSelection(field: any) { + if(field.selectionStart) { + return field.selectionStart; + // @ts-ignore + } else if(!document.selection) { + return 0; + } + + const c = '\x01'; + // @ts-ignore + const sel = document.selection.createRange(); + const txt = sel.text; + const dup = sel.duplicate(); + let len = 0; + + try { + dup.moveToElementText(field); + } catch(e) { + return 0; + } + + sel.text = txt + c; + len = dup.text.indexOf(c); + sel.moveStart('character', -1); + sel.text = ''; + + // if (browser.msie && len == -1) { + // return field.value.length + // } + return len; +} */ + export function getRichValue(field: HTMLElement) { if(!field) { return ''; @@ -90,6 +123,15 @@ MOUNT_CLASS_TO && (MOUNT_CLASS_TO.getRichValue = getRichValue); const markdownTags = [{ tagName: 'STRONG', markdown: '**' +}, { + tagName: 'B', // * legacy (Ctrl+B) + markdown: '**' +}, { + tagName: 'U', // * legacy (Ctrl+I) + markdown: '_-_' +}, { + tagName: 'I', // * legacy (Ctrl+I) + markdown: '__' }, { tagName: 'EM', markdown: '__' @@ -126,7 +168,7 @@ export function getRichElementValue(node: HTMLElement, lines: string[], line: st } } - line.push(markdown && node.nodeValue.trim() ? markdown + node.nodeValue + markdown : node.nodeValue); + line.push(markdown && node.nodeValue.trim() ? '\x01' + markdown + node.nodeValue + markdown + '\x01' : node.nodeValue); } return; @@ -392,3 +434,11 @@ export function getSelectedText(): string { return ''; } + +export function blurActiveElement() { + if(document.activeElement && (document.activeElement as HTMLInputElement).blur) { + (document.activeElement as HTMLInputElement).blur(); + } +} + +export const CLICK_EVENT_NAME = isTouchSupported ? 'touchend' : 'click'; diff --git a/src/lib/appManagers/apiUpdatesManager.ts b/src/lib/appManagers/apiUpdatesManager.ts index 7e133a51..b5a8b9d3 100644 --- a/src/lib/appManagers/apiUpdatesManager.ts +++ b/src/lib/appManagers/apiUpdatesManager.ts @@ -569,7 +569,7 @@ export class ApiUpdatesManager { } else { // ! for testing /* state.seq = 1; - state.pts = state.pts - 100; + state.pts = state.pts - 1000; state.date = 1; */ Object.assign(this.updatesState, state); diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index bc699376..8152579d 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -202,6 +202,7 @@ export class AppDialogsManager { //private topOffsetIndex = 0; private sliceTimeout: number; + private reorderDialogsTimeout: number; constructor() { this.chatsPreloader = putPreloader(null, true); @@ -891,37 +892,40 @@ export class AppDialogsManager { private reorderDialogs() { //const perf = performance.now(); - - let offset = 0; - if(this.topOffsetIndex) { - const element = this.chatList.firstElementChild; - if(element) { - const peerID = +element.getAttribute('data-peerID'); - const firstDialog = appMessagesManager.getDialogByPeerID(peerID); - offset = firstDialog[1]; - } - } - - const dialogs = appMessagesManager.dialogsStorage.getFolder(this.filterID); - const currentOrder = (Array.from(this.chatList.children) as HTMLElement[]).map(el => +el.getAttribute('data-peerID')); - - dialogs.forEach((dialog, index) => { - const dom = this.getDialogDom(dialog.peerID); - if(!dom) { - return; - } - - const currentIndex = currentOrder[dialog.peerID]; - const needIndex = index - offset; - - if(currentIndex != needIndex) { - if(positionElementByIndex(dom.listEl, this.chatList, needIndex)) { - this.log.debug('setDialogPosition:', dialog, dom, needIndex); + if(this.reorderDialogsTimeout) return; + this.reorderDialogsTimeout = window.requestAnimationFrame(() => { + this.reorderDialogsTimeout = 0; + let offset = 0; + if(this.topOffsetIndex) { + const element = this.chatList.firstElementChild; + if(element) { + const peerID = +element.getAttribute('data-peerID'); + const firstDialog = appMessagesManager.getDialogByPeerID(peerID); + offset = firstDialog[1]; } } + + const dialogs = appMessagesManager.dialogsStorage.getFolder(this.filterID); + const currentOrder = (Array.from(this.chatList.children) as HTMLElement[]).map(el => +el.getAttribute('data-peerID')); + + dialogs.forEach((dialog, index) => { + const dom = this.getDialogDom(dialog.peerID); + if(!dom) { + return; + } + + const needIndex = index - offset; + const peerIDByIndex = currentOrder[needIndex]; + + if(peerIDByIndex != dialog.peerID) { + if(positionElementByIndex(dom.listEl, this.chatList, needIndex)) { + this.log.debug('setDialogPosition:', dialog, dom, peerIDByIndex, needIndex); + } + } + }); + + //this.log('Reorder time:', performance.now() - perf); }); - - //this.log('Reorder time:', performance.now() - perf); } public setLastMessage(dialog: any, lastMessage?: any, dom?: DialogDom, highlightWord?: string) { diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index a4bfab5d..8ca692a2 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -40,7 +40,7 @@ import apiManager from '../mtproto/mtprotoworker'; import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config'; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from '../rootScope'; -import { cancelEvent, findUpClassName, findUpTag, placeCaretAtEnd, whichChild } from "../../helpers/dom"; +import { cancelEvent, CLICK_EVENT_NAME, findUpClassName, findUpTag, placeCaretAtEnd, whichChild } from "../../helpers/dom"; import apiUpdatesManager from './apiUpdatesManager'; import appChatsManager, { Channel, Chat } from "./appChatsManager"; import appDialogsManager from "./appDialogsManager"; @@ -708,7 +708,7 @@ export class AppImManager { this.mutePeer(this.peerID); }); - this.btnJoin.addEventListener('click', (e) => { + this.btnJoin.addEventListener(CLICK_EVENT_NAME, (e) => { cancelEvent(e); this.btnJoin.setAttribute('disabled', 'true'); @@ -717,11 +717,11 @@ export class AppImManager { }); }); - this.menuButtons.mute.addEventListener('click', (e) => { + this.menuButtons.mute.addEventListener(CLICK_EVENT_NAME, (e) => { this.mutePeer(this.peerID); }); - this.menuButtons.search.addEventListener('click', (e) => { + this.menuButtons.search.addEventListener(CLICK_EVENT_NAME, (e) => { new ChatSearch(); }); @@ -745,6 +745,12 @@ export class AppImManager { this.chatInputC.replyElements.cancelBtn.click(); } else if(this.peerID != 0) { // hide current dialog this.setPeer(0); + cancelEvent(e); + } + + // * cancel event for safari, because if application is in fullscreen, browser will try to exit fullscreen + if(this.peerID) { + cancelEvent(e); } } else if(e.key == 'Meta' || e.key == 'Control') { return; @@ -785,7 +791,8 @@ export class AppImManager { document.body.addEventListener('keydown', onKeyDown); - this.goDownBtn.addEventListener('click', () => { + this.goDownBtn.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); const dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0]; if(dialog) { @@ -1898,7 +1905,8 @@ export class AppImManager { containerDiv.append(rowDiv); }); - containerDiv.addEventListener('click', (e) => { + containerDiv.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); let target = e.target as HTMLElement; if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button'); @@ -2790,8 +2798,13 @@ export class AppImManager { } public setMutedState(muted = false) { - appSidebarRight.sharedMediaTab.profileElements.notificationsCheckbox.checked = !muted; - appSidebarRight.sharedMediaTab.profileElements.notificationsStatus.innerText = muted ? 'Disabled' : 'Enabled'; + if(!this.peerID) return; + + const profileElements = appSidebarRight.sharedMediaTab.profileElements; + if(profileElements) { + appSidebarRight.sharedMediaTab.profileElements.notificationsCheckbox.checked = !muted; + appSidebarRight.sharedMediaTab.profileElements.notificationsStatus.innerText = muted ? 'Disabled' : 'Enabled'; + } if(appPeersManager.isBroadcast(this.peerID)) { // not human this.btnMute.classList.remove('tgico-mute', 'tgico-unmute'); diff --git a/src/scss/components/_global.scss b/src/scss/components/_global.scss index 4a8dce25..70b63dd9 100644 --- a/src/scss/components/_global.scss +++ b/src/scss/components/_global.scss @@ -87,7 +87,7 @@ Utility Classes } // No Text Select -.no-select { +.no-select/* , .no-select * */ { user-select: none; } diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index 909e2796..a8b3a47d 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -10,6 +10,7 @@ avatar-element { /* overflow: hidden; */ position: relative; user-select: none; + text-transform: uppercase; @include respond-to(handhelds) { font-size: 14px; diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 5f970279..3e824e5f 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -106,6 +106,10 @@ transform-origin: top left; } + &.bottom-center { + transform-origin: top center; + } + &.top-left { top: initial; right: 0; @@ -120,6 +124,14 @@ transform-origin: bottom left; } + &.center-left { + transform-origin: center right; + } + + &.center-right { + transform-origin: center left; + } + &-item { display: flex; position: relative; @@ -170,6 +182,7 @@ bottom: 0; z-index: 1; cursor: default; + user-select: none; //background-color: rgba(0, 0, 0, .2); } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index adb49656..1581b4a6 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -229,6 +229,12 @@ $bubble-margin: .25rem; } } } + + #bubbles.is-selecting & { + .audio, .document, .attachment, poll-element { + pointer-events: none !important; + } + } &__container { //min-width: 60px; @@ -244,7 +250,12 @@ $bubble-margin: .25rem; height: fit-content; z-index: 2; transition: .2s transform; - user-select: text; + user-select: none; + + html.no-touch #bubbles:not(.is-selecting) &, + html.is-touch #bubbles.is-selecting:not(.no-select) & { + user-select: text; + } @include respond-to(not-handhelds) { max-width: 85%; diff --git a/src/scss/partials/_input.scss b/src/scss/partials/_input.scss index cd71f784..7afe5d9c 100644 --- a/src/scss/partials/_input.scss +++ b/src/scss/partials/_input.scss @@ -70,7 +70,7 @@ } @include respond-to(handhelds) { - height: 50px; + min-height: 50px; } /* font-weight: 500; */ diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 3802035a..34b348ee 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -214,12 +214,9 @@ max-width: 78px; width: 78px; align-items: center; - position: relative; display: flex; flex-direction: column; - cursor: pointer; padding: 12px 0 0 !important; - overflow: hidden; margin: 0; @include respond-to(handhelds) { @@ -233,6 +230,10 @@ height: 54px; } + .dialog-title-details { + display: none; + } + .user-caption { max-width: 65px; padding: 2px 0px 9px; @@ -243,10 +244,6 @@ } } - .user-title { - max-width: unset; - } - .search-group-scrollable { position: relative; @@ -595,10 +592,6 @@ width: 100%; padding: 0 16px; } - - .input-field input { - height: 50px; - } } .sidebar-left-h2 { diff --git a/src/scss/style.scss b/src/scss/style.scss index 29d2fa12..6d8910e9 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -198,6 +198,38 @@ $messages-container-width: 728px; unicode-range:U + 0000-00FF, U + 0131, U + 0152-0153, U + 02BB-02BC, U + 02C6, U + 02DA, U + 02DC, U + 2000-206F, U + 2074, U + 20AC, U + 2122, U + 2191, U + 2193, U + 2212, U + 2215, U + FEFF, U + FFFD } +// !!! FIX FOR [contenteditable] Ctrl+B, due to font-weight: 500; + +/* cyrillic */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fABc4AMP6lbBP.woff2) format('woff2'); + unicode-range:U + 0400-045F, U + 0490-0491, U + 04B0-04B1, U + 2116 +} + +/* latin-ext */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fChc4AMP6lbBP.woff2) format('woff2'); + unicode-range:U + 0100-024F, U + 0259, U + 1E00-1EFF, U + 2020, U + 20A0-20AB, U + 20AD-20CF, U + 2113, U + 2C60-2C7F, U + A720-A7FF +} + +/* latin */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Roboto Medium'), local('Roboto-Medium'), url(assets/fonts/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2) format('woff2'); + unicode-range:U + 0000-00FF, U + 0131, U + 0152-0153, U + 02BB-02BC, U + 02C6, U + 02DA, U + 02DC, U + 2000-206F, U + 2074, U + 20AC, U + 2122, U + 2191, U + 2193, U + 2212, U + 2215, U + FEFF, U + FFFD +} + html, body { height: 100%; width: 100%; @@ -404,7 +436,8 @@ hr { .user-title, b/* , .user-last-message b */ { color: #000; - font-weight: 500; + font-weight: bolder; + //font-weight: 500; //font-weight: normal; }