Browse Source

Fix context menu again

Multiselect fixes for desktop & mobile Safari
Fix multiselect album on mobiles
master
Eduard Kuzmenko 4 years ago
parent
commit
984b04ab40
  1. 6
      src/components/appMediaViewer.ts
  2. 24
      src/components/audio.ts
  3. 5
      src/components/buttonMenu.ts
  4. 6
      src/components/buttonMenuToggle.ts
  5. 41
      src/components/chat/contextMenu.ts
  6. 4
      src/components/chat/selection.ts
  7. 38
      src/components/misc.ts
  8. 18
      src/components/poll.ts
  9. 3
      src/components/popup.ts
  10. 14
      src/components/sidebarLeft/index.ts
  11. 8
      src/components/wrappers.ts
  12. 52
      src/helpers/dom.ts
  13. 2
      src/lib/appManagers/apiUpdatesManager.ts
  14. 60
      src/lib/appManagers/appDialogsManager.ts
  15. 29
      src/lib/appManagers/appImManager.ts
  16. 2
      src/scss/components/_global.scss
  17. 1
      src/scss/partials/_avatar.scss
  18. 13
      src/scss/partials/_button.scss
  19. 13
      src/scss/partials/_chatBubble.scss
  20. 2
      src/scss/partials/_input.scss
  21. 15
      src/scss/partials/_leftSidebar.scss
  22. 35
      src/scss/style.scss

6
src/components/appMediaViewer.ts

@ -1173,7 +1173,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
this.openMedia(appMessagesManager.getMessage(target.mid), target.element); this.openMedia(appMessagesManager.getMessage(target.mid), target.element);
}; };
onForwardClick = (e: MouseEvent) => { onForwardClick = () => {
if(this.currentMessageID) { if(this.currentMessageID) {
//appSidebarRight.forwardTab.open([this.currentMessageID]); //appSidebarRight.forwardTab.open([this.currentMessageID]);
new PopupForward([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); const message = appMessagesManager.getMessage(this.currentMessageID);
if(message.media.photo) { if(message.media.photo) {
appPhotosManager.savePhotoFile(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); this.openMedia(target.photoID, target.element, 1);
}; };
onDownloadClick = (e: MouseEvent) => { onDownloadClick = () => {
appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.currentPhotoID)); appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.currentPhotoID));
}; };

24
src/components/audio.ts

@ -11,6 +11,7 @@ import { isSafari } from "../helpers/userAgent";
import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import './middleEllipsis'; import './middleEllipsis';
import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
rootScope.on('messages_media_read', e => { rootScope.on('messages_media_read', e => {
const mids = e.detail; const mids = e.detail;
@ -213,12 +214,14 @@ function wrapVoiceMessage(doc: MyDocument, audioEl: AudioElement, mid: number) {
mousedown = false; mousedown = false;
} }
}); });
progress.addEventListener('click', (e) => { progress.addEventListener(CLICK_EVENT_NAME, (e) => {
cancelEvent(e);
if(!audio.paused) scrub(e); if(!audio.paused) scrub(e);
}); });
function scrub(e: MouseEvent) { function scrub(e: MouseEvent | TouchEvent) {
const scrubTime = e.offsetX / availW /* width */ * audio.duration; 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); lastIndex = Math.round(scrubTime / audio.duration * barCount);
rects.slice(0, lastIndex + 1).forEach(node => node.classList.add('active')); 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; 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(() => {}); if(audio.paused) audio.play().catch(() => {});
else audio.pause(); else audio.pause();
}); });
@ -395,7 +399,8 @@ export default class AudioElement extends HTMLElement {
if(doc.type == 'voice') { if(doc.type == 'voice') {
let download: Download; let download: Download;
const onClick = () => { const onClick = (e: Event) => {
cancelEvent(e);
if(!download) { if(!download) {
if(!preloader) { if(!preloader) {
preloader = new ProgressivePreloader(null, true); preloader = new ProgressivePreloader(null, true);
@ -406,7 +411,7 @@ export default class AudioElement extends HTMLElement {
download.then(() => { download.then(() => {
downloadDiv.remove(); downloadDiv.remove();
this.removeEventListener('click', onClick); this.removeEventListener(CLICK_EVENT_NAME, onClick);
onLoad(); onLoad();
}).catch(err => { }).catch(err => {
if(err.name === 'AbortError') { 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(); this.click();
} else { } else {
onLoad(false); onLoad(false);
@ -430,8 +435,9 @@ export default class AudioElement extends HTMLElement {
//if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано //if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано
//onLoad(); //onLoad();
//} else { //} else {
const r = () => { const r = (e: Event) => {
//onLoad(); //onLoad();
cancelEvent(e);
appMediaPlaybackController.resolveWaitingForLoadMedia(mid); appMediaPlaybackController.resolveWaitingForLoadMedia(mid);
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio 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 { } else {

5
src/components/buttonMenu.ts

@ -1,6 +1,7 @@
import { CLICK_EVENT_NAME } from "../helpers/dom";
import { ripple } from "./ripple"; 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) => { const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
if(options.element) return options.element; if(options.element) return options.element;
@ -11,7 +12,7 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
el.innerText = text; el.innerText = text;
ripple(el); ripple(el);
el.addEventListener('click', onClick); el.addEventListener(CLICK_EVENT_NAME, onClick);
return options.element = el; return options.element = el;
}; };

6
src/components/buttonMenuToggle.ts

@ -1,3 +1,4 @@
import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
import ButtonIcon from "./buttonIcon"; import ButtonIcon from "./buttonIcon";
import ButtonMenu, { ButtonMenuItemOptions } from "./buttonMenu"; import ButtonMenu, { ButtonMenuItemOptions } from "./buttonMenu";
import { closeBtnMenu, openBtnMenu } from "./misc"; import { closeBtnMenu, openBtnMenu } from "./misc";
@ -12,14 +13,13 @@ const ButtonMenuToggle = (options: Partial<{noRipple: true, onlyMobile: true}> =
}; };
const ButtonMenuToggleHandler = (el: HTMLElement) => { const ButtonMenuToggleHandler = (el: HTMLElement) => {
(el as HTMLElement).addEventListener('click', (e) => { (el as HTMLElement).addEventListener(CLICK_EVENT_NAME, (e) => {
//console.log('click pageIm'); //console.log('click pageIm');
if(!el.classList.contains('btn-menu-toggle')) return false; if(!el.classList.contains('btn-menu-toggle')) return false;
//window.removeEventListener('mousemove', onMouseMove); //window.removeEventListener('mousemove', onMouseMove);
const openedMenu = el.querySelector('.btn-menu') as HTMLDivElement; const openedMenu = el.querySelector('.btn-menu') as HTMLDivElement;
e.cancelBubble = true; cancelEvent(e);
//cancelEvent(e);
if(el.classList.contains('menu-open')) { if(el.classList.contains('menu-open')) {
closeBtnMenu(); closeBtnMenu();

41
src/components/chat/contextMenu.ts

@ -87,33 +87,60 @@ export default class ChatContextMenu {
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right'; const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
//bubble.parentElement.append(this.element); //bubble.parentElement.append(this.element);
//appImManager.log('contextmenu', e, bubble, side);
positionMenu(e, this.element, side); positionMenu(e, this.element, side);
openBtnMenu(this.element, () => { openBtnMenu(this.element, () => {
this.peerID = this.msgID = 0; this.peerID = this.msgID = 0;
this.target = null; this.target = null;
}); });
/////this.log('contextmenu', e, bubble, msgID, side);
}; };
if(isTouchSupported) { if(isTouchSupported) {
attachTo.addEventListener('click', (e) => { const attachClickEvent = (elem: HTMLElement, callback: (e: TouchEvent) => void) => {
//const good = !!findUpClassName(e.target, 'message') || !!findUpClassName(e.target, 'bubble__container'); 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; const className = (e.target as HTMLElement).className;
if(!className || !className.includes) return; 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)'))); const good = ['bubble', 'bubble__container', 'message', 'time', 'inner'].find(c => className.match(new RegExp(c + '($|\\s)')));
if(good) { if(good) {
onContextMenu(e); cancelEvent(e);
onContextMenu(e.changedTouches[0]);
} }
}); });
attachContextMenuListener(attachTo, (e) => { attachContextMenuListener(attachTo, (e) => {
if(appImManager.chatSelection.isSelecting) return; 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(); cancelSelection();
cancelEvent(e as any); //cancelEvent(e as any);
let bubble = findUpClassName(e.target, 'bubble'); const bubble = findUpClassName(e.target, 'album-item') || findUpClassName(e.target, 'bubble');
if(bubble) { if(bubble) {
appImManager.chatSelection.toggleByBubble(bubble); appImManager.chatSelection.toggleByBubble(bubble);
} }

4
src/components/chat/selection.ts

@ -1,7 +1,7 @@
import { isTouchSupported } from "../../helpers/touchSupport"; import { isTouchSupported } from "../../helpers/touchSupport";
import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppImManager } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; 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 Button from "../button";
import ButtonIcon from "../buttonIcon"; import ButtonIcon from "../buttonIcon";
import CheckboxField from "../checkbox"; import CheckboxField from "../checkbox";
@ -225,6 +225,8 @@ export default class ChatSelection {
} }
} }
blurActiveElement(); // * for mobile keyboards
SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size, 200, () => { SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size, 200, () => {
if(!this.isSelecting) { if(!this.isSelecting) {
this.selectionContainer.remove(); this.selectionContainer.remove();

38
src/components/misc.ts

@ -1,5 +1,5 @@
import Countries, { Country, PhoneCodesMain } from "../countries"; 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 mediaSizes from "../helpers/mediaSizes";
import { clamp } from "../helpers/number"; import { clamp } from "../helpers/number";
import { isTouchSupported } from "../helpers/touchSupport"; import { isTouchSupported } from "../helpers/touchSupport";
@ -148,7 +148,7 @@ export const closeBtnMenu = () => {
window.removeEventListener('contextmenu', onClick); window.removeEventListener('contextmenu', onClick);
} }
document.removeEventListener('click', onClick); document.removeEventListener(CLICK_EVENT_NAME, onClick);
}; };
window.addEventListener('resize', () => { 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 // ! 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_TOP = 8;
const PADDING_LEFT = 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; //let {clientX, clientY} = e;
// * side mean the OPEN side // * side mean the OPEN side
let {scrollWidth: menuWidth, scrollHeight: menuHeight} = elem; 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'; side = mediaSizes.isMobile ? 'right' : 'left';
let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top'; let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top';
@ -205,17 +208,17 @@ export function positionMenu({clientX, clientY}: {clientX: number, clientY: numb
const getSides = () => { const getSides = () => {
return { return {
x: { x: {
left: clientX, left: pageX,
right: clientX - menuWidth right: pageX - menuWidth
}, },
intermediateX: side == 'right' ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, intermediateX: side == 'right' ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT,
//intermediateX: clientX < windowWidth / 2 ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, //intermediateX: clientX < windowWidth / 2 ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT,
y: { y: {
top: clientY, top: pageY,
bottom: clientY - menuHeight bottom: pageY - menuHeight
}, },
//intermediateY: verticalSide == 'top' ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, //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) { if(isApple && isTouchSupported) {
let timeout: number; let timeout: number;
const options: any = /* null */{capture: true};
const onCancel = () => { const onCancel = () => {
clearTimeout(timeout); clearTimeout(timeout);
element.removeEventListener('touchmove', onCancel); element.removeEventListener('touchmove', onCancel, options);
element.removeEventListener('touchend', onCancel); element.removeEventListener('touchend', onCancel, options);
element.removeEventListener('touchcancel', onCancel); element.removeEventListener('touchcancel', onCancel, options);
}; };
element.addEventListener('touchstart', (e) => { element.addEventListener('touchstart', (e) => {
@ -303,11 +308,12 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To
return; return;
} }
element.addEventListener('touchmove', onCancel); element.addEventListener('touchmove', onCancel, options);
element.addEventListener('touchend', onCancel); element.addEventListener('touchend', onCancel, options);
element.addEventListener('touchcancel', onCancel); element.addEventListener('touchcancel', onCancel, options);
timeout = window.setTimeout(() => { timeout = window.setTimeout(() => {
element.addEventListener('touchend', cancelEvent, {once: true}); // * fix instant closing
callback(e.touches[0]); callback(e.touches[0]);
onCancel(); onCancel();
}, .4e3); }, .4e3);

18
src/components/poll.ts

@ -5,7 +5,7 @@ import appPollsManager, { Poll, PollResults } from "../lib/appManagers/appPollsM
import serverTimeManager from "../lib/mtproto/serverTimeManager"; import serverTimeManager from "../lib/mtproto/serverTimeManager";
import { RichTextProcessor } from "../lib/richtextprocessor"; import { RichTextProcessor } from "../lib/richtextprocessor";
import rootScope from "../lib/rootScope"; 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 { ripple } from "./ripple";
import appSidebarRight from "./sidebarRight"; import appSidebarRight from "./sidebarRight";
@ -338,7 +338,8 @@ export default class PollElement extends HTMLElement {
this.votersCountDiv.classList.add('hide'); 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); /* const indexes = this.answerDivs.filter(el => el.classList.contains('is-chosing')).map(el => +el.dataset.index);
if(indexes.length) { if(indexes.length) {
@ -363,7 +364,7 @@ export default class PollElement extends HTMLElement {
this.performResults(results, poll.chosenIndexes); this.performResults(results, poll.chosenIndexes);
} else if(!this.isClosed) { } else if(!this.isClosed) {
this.setVotersCount(results); 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); this.descDiv.append(toggleHint);
//let active = false; //let active = false;
toggleHint.addEventListener('click', (e) => { toggleHint.addEventListener(CLICK_EVENT_NAME, (e) => {
cancelEvent(e); cancelEvent(e);
//active = true; //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; const target = findUpClassName(e.target, 'poll-answer') as HTMLElement;
if(!target) { if(!target) {
return; return;
} }
cancelEvent(e);
const answerIndex = +target.dataset.index; const answerIndex = +target.dataset.index;
if(this.isMultiple) { if(this.isMultiple) {
target.classList.toggle('is-chosing'); target.classList.toggle('is-chosing');
@ -512,9 +514,9 @@ export default class PollElement extends HTMLElement {
this.chosenIndexes = chosenIndexes.slice(); this.chosenIndexes = chosenIndexes.slice();
if(this.isRetracted) { if(this.isRetracted) {
this.addEventListener('click', this.clickHandler); this.addEventListener(CLICK_EVENT_NAME, this.clickHandler);
} else { } else {
this.removeEventListener('click', this.clickHandler); this.removeEventListener(CLICK_EVENT_NAME, this.clickHandler);
} }
} }

3
src/components/popup.ts

@ -1,5 +1,5 @@
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import { cancelEvent, findUpClassName } from "../helpers/dom"; import { blurActiveElement, cancelEvent, findUpClassName } from "../helpers/dom";
import { ripple } from "./ripple"; import { ripple } from "./ripple";
export class PopupElement { export class PopupElement {
@ -100,6 +100,7 @@ export class PopupElement {
}; };
public show() { public show() {
blurActiveElement(); // * hide mobile keyboard
document.body.append(this.element); document.body.append(this.element);
void this.element.offsetWidth; // reflow void this.element.offsetWidth; // reflow
this.element.classList.add('active'); this.element.classList.add('active');

14
src/components/sidebarLeft/index.ts

@ -8,7 +8,7 @@ import appStateManager from "../../lib/appManagers/appStateManager";
import appUsersManager from "../../lib/appManagers/appUsersManager"; import appUsersManager from "../../lib/appManagers/appUsersManager";
import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config"; import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config";
import rootScope from "../../lib/rootScope"; 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 AppSearch, { SearchGroup } from "../appSearch";
import "../avatar"; import "../avatar";
import { parseMenuButtonsTo, putPreloader } from "../misc"; 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.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'); ///////this.log('savedbtn click');
setTimeout(() => { // menu doesn't close if no timeout (lol) setTimeout(() => { // menu doesn't close if no timeout (lol)
appImManager.setPeer(appImManager.myID); appImManager.setPeer(appImManager.myID);
}, 0); }, 0);
}); });
this.buttons.archived.addEventListener('click', (e) => { this.buttons.archived.addEventListener(CLICK_EVENT_NAME, (e) => {
this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.archived); this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.archived);
}); });
this.buttons.contacts.addEventListener('click', (e) => { this.buttons.contacts.addEventListener(CLICK_EVENT_NAME, (e) => {
this.contactsTab.openContacts(); this.contactsTab.openContacts();
}); });
this.buttons.settings.addEventListener('click', (e) => { this.buttons.settings.addEventListener(CLICK_EVENT_NAME, (e) => {
this.settingsTab.fillElements(); this.settingsTab.fillElements();
this.selectTab(AppSidebarLeft.SLIDERITEMSIDS.settings); 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.selectTab(AppSidebarLeft.SLIDERITEMSIDS.newChannel);
}); });
[this.newButtons.group, this.buttons.newGroup].forEach(btn => { [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.addMembersTab.init(0, 'chat', false, (peerIDs) => {
this.newGroupTab.init(peerIDs); this.newGroupTab.init(peerIDs);
}); });

8
src/components/wrappers.ts

@ -12,7 +12,7 @@ import appMessagesManager from '../lib/appManagers/appMessagesManager';
import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager'; import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager';
import LottieLoader from '../lib/lottieLoader'; import LottieLoader from '../lib/lottieLoader';
import VideoPlayer from '../lib/mediaPlayer'; 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 webpWorkerController from '../lib/webp/webpWorkerController';
import animationIntersector from './animationIntersector'; import animationIntersector from './animationIntersector';
import appMediaPlaybackController from './appMediaPlaybackController'; import appMediaPlaybackController from './appMediaPlaybackController';
@ -373,7 +373,8 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals
let preloader: ProgressivePreloader; let preloader: ProgressivePreloader;
let download: DownloadBlob; let download: DownloadBlob;
docDiv.addEventListener('click', (e) => { docDiv.addEventListener(CLICK_EVENT_NAME, (e) => {
cancelEvent(e);
if(!download) { if(!download) {
if(downloadDiv.classList.contains('downloading')) { if(downloadDiv.classList.contains('downloading')) {
return; // means not ready yet return; // means not ready yet
@ -670,7 +671,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
}, true); }, true);
if(emoji) { if(emoji) {
div.addEventListener('click', () => { div.addEventListener(CLICK_EVENT_NAME, (e) => {
cancelEvent(e);
let animation = LottieLoader.getAnimation(div); let animation = LottieLoader.getAnimation(div);
if(animation.paused) { if(animation.paused) {

52
src/helpers/dom.ts

@ -1,4 +1,5 @@
import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config";
import { isTouchSupported } from "./touchSupport";
/* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { /* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean {
if(!element) { 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) { export function getRichValue(field: HTMLElement) {
if(!field) { if(!field) {
return ''; return '';
@ -90,6 +123,15 @@ MOUNT_CLASS_TO && (MOUNT_CLASS_TO.getRichValue = getRichValue);
const markdownTags = [{ const markdownTags = [{
tagName: 'STRONG', tagName: 'STRONG',
markdown: '**' markdown: '**'
}, {
tagName: 'B', // * legacy (Ctrl+B)
markdown: '**'
}, {
tagName: 'U', // * legacy (Ctrl+I)
markdown: '_-_'
}, {
tagName: 'I', // * legacy (Ctrl+I)
markdown: '__'
}, { }, {
tagName: 'EM', tagName: 'EM',
markdown: '__' 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; return;
@ -392,3 +434,11 @@ export function getSelectedText(): string {
return ''; 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';

2
src/lib/appManagers/apiUpdatesManager.ts

@ -569,7 +569,7 @@ export class ApiUpdatesManager {
} else { } else {
// ! for testing // ! for testing
/* state.seq = 1; /* state.seq = 1;
state.pts = state.pts - 100; state.pts = state.pts - 1000;
state.date = 1; */ state.date = 1; */
Object.assign(this.updatesState, state); Object.assign(this.updatesState, state);

60
src/lib/appManagers/appDialogsManager.ts

@ -202,6 +202,7 @@ export class AppDialogsManager {
//private topOffsetIndex = 0; //private topOffsetIndex = 0;
private sliceTimeout: number; private sliceTimeout: number;
private reorderDialogsTimeout: number;
constructor() { constructor() {
this.chatsPreloader = putPreloader(null, true); this.chatsPreloader = putPreloader(null, true);
@ -891,37 +892,40 @@ export class AppDialogsManager {
private reorderDialogs() { private reorderDialogs() {
//const perf = performance.now(); //const perf = performance.now();
if(this.reorderDialogsTimeout) return;
let offset = 0; this.reorderDialogsTimeout = window.requestAnimationFrame(() => {
if(this.topOffsetIndex) { this.reorderDialogsTimeout = 0;
const element = this.chatList.firstElementChild; let offset = 0;
if(element) { if(this.topOffsetIndex) {
const peerID = +element.getAttribute('data-peerID'); const element = this.chatList.firstElementChild;
const firstDialog = appMessagesManager.getDialogByPeerID(peerID); if(element) {
offset = firstDialog[1]; 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);
} }
} }
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) { public setLastMessage(dialog: any, lastMessage?: any, dom?: DialogDom, highlightWord?: string) {

29
src/lib/appManagers/appImManager.ts

@ -40,7 +40,7 @@ import apiManager from '../mtproto/mtprotoworker';
import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config'; import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config';
import { RichTextProcessor } from "../richtextprocessor"; import { RichTextProcessor } from "../richtextprocessor";
import rootScope from '../rootScope'; 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 apiUpdatesManager from './apiUpdatesManager';
import appChatsManager, { Channel, Chat } from "./appChatsManager"; import appChatsManager, { Channel, Chat } from "./appChatsManager";
import appDialogsManager from "./appDialogsManager"; import appDialogsManager from "./appDialogsManager";
@ -708,7 +708,7 @@ export class AppImManager {
this.mutePeer(this.peerID); this.mutePeer(this.peerID);
}); });
this.btnJoin.addEventListener('click', (e) => { this.btnJoin.addEventListener(CLICK_EVENT_NAME, (e) => {
cancelEvent(e); cancelEvent(e);
this.btnJoin.setAttribute('disabled', 'true'); 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.mutePeer(this.peerID);
}); });
this.menuButtons.search.addEventListener('click', (e) => { this.menuButtons.search.addEventListener(CLICK_EVENT_NAME, (e) => {
new ChatSearch(); new ChatSearch();
}); });
@ -745,6 +745,12 @@ export class AppImManager {
this.chatInputC.replyElements.cancelBtn.click(); this.chatInputC.replyElements.cancelBtn.click();
} else if(this.peerID != 0) { // hide current dialog } else if(this.peerID != 0) { // hide current dialog
this.setPeer(0); 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') { } else if(e.key == 'Meta' || e.key == 'Control') {
return; return;
@ -785,7 +791,8 @@ export class AppImManager {
document.body.addEventListener('keydown', onKeyDown); 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]; const dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0];
if(dialog) { if(dialog) {
@ -1898,7 +1905,8 @@ export class AppImManager {
containerDiv.append(rowDiv); containerDiv.append(rowDiv);
}); });
containerDiv.addEventListener('click', (e) => { containerDiv.addEventListener(CLICK_EVENT_NAME, (e) => {
cancelEvent(e);
let target = e.target as HTMLElement; let target = e.target as HTMLElement;
if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button'); if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button');
@ -2790,8 +2798,13 @@ export class AppImManager {
} }
public setMutedState(muted = false) { public setMutedState(muted = false) {
appSidebarRight.sharedMediaTab.profileElements.notificationsCheckbox.checked = !muted; if(!this.peerID) return;
appSidebarRight.sharedMediaTab.profileElements.notificationsStatus.innerText = muted ? 'Disabled' : 'Enabled';
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 if(appPeersManager.isBroadcast(this.peerID)) { // not human
this.btnMute.classList.remove('tgico-mute', 'tgico-unmute'); this.btnMute.classList.remove('tgico-mute', 'tgico-unmute');

2
src/scss/components/_global.scss

@ -87,7 +87,7 @@ Utility Classes
} }
// No Text Select // No Text Select
.no-select { .no-select/* , .no-select * */ {
user-select: none; user-select: none;
} }

1
src/scss/partials/_avatar.scss

@ -10,6 +10,7 @@ avatar-element {
/* overflow: hidden; */ /* overflow: hidden; */
position: relative; position: relative;
user-select: none; user-select: none;
text-transform: uppercase;
@include respond-to(handhelds) { @include respond-to(handhelds) {
font-size: 14px; font-size: 14px;

13
src/scss/partials/_button.scss

@ -106,6 +106,10 @@
transform-origin: top left; transform-origin: top left;
} }
&.bottom-center {
transform-origin: top center;
}
&.top-left { &.top-left {
top: initial; top: initial;
right: 0; right: 0;
@ -120,6 +124,14 @@
transform-origin: bottom left; transform-origin: bottom left;
} }
&.center-left {
transform-origin: center right;
}
&.center-right {
transform-origin: center left;
}
&-item { &-item {
display: flex; display: flex;
position: relative; position: relative;
@ -170,6 +182,7 @@
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
cursor: default; cursor: default;
user-select: none;
//background-color: rgba(0, 0, 0, .2); //background-color: rgba(0, 0, 0, .2);
} }

13
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 { &__container {
//min-width: 60px; //min-width: 60px;
@ -244,7 +250,12 @@ $bubble-margin: .25rem;
height: fit-content; height: fit-content;
z-index: 2; z-index: 2;
transition: .2s transform; 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) { @include respond-to(not-handhelds) {
max-width: 85%; max-width: 85%;

2
src/scss/partials/_input.scss

@ -70,7 +70,7 @@
} }
@include respond-to(handhelds) { @include respond-to(handhelds) {
height: 50px; min-height: 50px;
} }
/* font-weight: 500; */ /* font-weight: 500; */

15
src/scss/partials/_leftSidebar.scss

@ -214,12 +214,9 @@
max-width: 78px; max-width: 78px;
width: 78px; width: 78px;
align-items: center; align-items: center;
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
cursor: pointer;
padding: 12px 0 0 !important; padding: 12px 0 0 !important;
overflow: hidden;
margin: 0; margin: 0;
@include respond-to(handhelds) { @include respond-to(handhelds) {
@ -233,6 +230,10 @@
height: 54px; height: 54px;
} }
.dialog-title-details {
display: none;
}
.user-caption { .user-caption {
max-width: 65px; max-width: 65px;
padding: 2px 0px 9px; padding: 2px 0px 9px;
@ -243,10 +244,6 @@
} }
} }
.user-title {
max-width: unset;
}
.search-group-scrollable { .search-group-scrollable {
position: relative; position: relative;
@ -595,10 +592,6 @@
width: 100%; width: 100%;
padding: 0 16px; padding: 0 16px;
} }
.input-field input {
height: 50px;
}
} }
.sidebar-left-h2 { .sidebar-left-h2 {

35
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 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 { html, body {
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -404,7 +436,8 @@ hr {
.user-title, b/* , .user-last-message b */ { .user-title, b/* , .user-last-message b */ {
color: #000; color: #000;
font-weight: 500; font-weight: bolder;
//font-weight: 500;
//font-weight: normal; //font-weight: normal;
} }

Loading…
Cancel
Save