From 0c4a99f67de5ee44541c63dd59a7828fdf5cce5f Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Wed, 10 Aug 2022 21:50:25 +0200 Subject: [PATCH] Stickers: premium effect & toast --- src/components/appNavigationController.ts | 3 +- src/components/buttonMenu.ts | 2 +- src/components/buttonMenuToggle.ts | 2 +- src/components/chat/bubbles.ts | 3 +- src/components/chat/chat.ts | 4 +- src/components/chat/contextMenu.ts | 4 +- src/components/groupCall/participants.ts | 4 +- src/components/sidebarLeft/index.ts | 2 +- src/components/toast.ts | 52 ++++++-- src/components/wrappers/sticker.ts | 89 ++++++++++--- src/components/wrappers/stickerAnimation.ts | 18 ++- src/helpers/contextMenuController.ts | 117 ++++-------------- src/helpers/dom/clickEvent.ts | 4 +- src/helpers/overlayClickHandler.ts | 103 +++++++++++++++ src/lang.ts | 1 + src/lib/appManagers/appStickersManager.ts | 11 +- .../utils/docs/getDocumentDownloadOptions.ts | 6 +- .../utils/download/getDownloadMediaDetails.ts | 2 +- .../utils/stickers/getStickerEffectThumb.ts | 5 + src/lib/mtproto/apiFileManager.ts | 4 +- src/scss/components/_global.scss | 4 + src/scss/partials/_emojiAnimation.scss | 6 - src/scss/style.scss | 18 ++- 23 files changed, 307 insertions(+), 157 deletions(-) create mode 100644 src/helpers/overlayClickHandler.ts create mode 100644 src/lib/appManagers/utils/stickers/getStickerEffectThumb.ts diff --git a/src/components/appNavigationController.ts b/src/components/appNavigationController.ts index e2371c7b..dc734d47 100644 --- a/src/components/appNavigationController.ts +++ b/src/components/appNavigationController.ts @@ -15,7 +15,8 @@ import indexOfAndSplice from '../helpers/array/indexOfAndSplice'; export type NavigationItem = { type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' | 'esg' | 'multiselect' | 'input-helper' | 'autocomplete-helper' | 'markup' | - 'global-search' | 'voice' | 'mobile-search' | 'filters' | 'global-search-focus', + 'global-search' | 'voice' | 'mobile-search' | 'filters' | 'global-search-focus' | + 'toast', onPop: (canAnimate: boolean) => boolean | void, onEscape?: () => boolean, noHistory?: boolean, diff --git a/src/components/buttonMenu.ts b/src/components/buttonMenu.ts index 58054957..5360bd19 100644 --- a/src/components/buttonMenu.ts +++ b/src/components/buttonMenu.ts @@ -62,7 +62,7 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => { } if(!keepOpen) { - contextMenuController.closeBtnMenu(); + contextMenuController.close(); } if(checkboxField && !noCheckboxClickListener/* && result !== false */) { diff --git a/src/components/buttonMenuToggle.ts b/src/components/buttonMenuToggle.ts index b4aeb941..14dbccc6 100644 --- a/src/components/buttonMenuToggle.ts +++ b/src/components/buttonMenuToggle.ts @@ -49,7 +49,7 @@ const ButtonMenuToggleHandler = (el: HTMLElement, onOpen?: (e: Event) => void | cancelEvent(e); if(el.classList.contains('menu-open')) { - contextMenuController.closeBtnMenu(); + contextMenuController.close(); } else { const result = onOpen && onOpen(e); const open = () => { diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 20a74974..ea23d86a 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -4016,7 +4016,8 @@ export default class ChatBubbles { loop: true, emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined, withThumb: true, - loadPromises + loadPromises, + isOut }); } else if(doc.type === 'video' || doc.type === 'gif' || doc.type === 'round'/* && doc.size <= 20e6 */) { // this.log('never get free 2', doc); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 396148b7..685b840a 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -605,13 +605,13 @@ export default class Chat extends EventListenerBase<{ } public isOurMessage(message: Message.message | Message.messageService) { - return message.fromId === rootScope.myId || (message.pFlags.out && this.isMegagroup); + return message.fromId === rootScope.myId || (!!message.pFlags.out && this.isMegagroup); } public isOutMessage(message: Message.message | Message.messageService) { const fwdFrom = (message as Message.message).fwd_from; const isOut = this.isOurMessage(message) && (!fwdFrom || this.peerId !== rootScope.myId); - return isOut; + return !!isOut; } public isAvatarNeeded(message: Message.message | Message.messageService) { diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 67102f67..c6785a11 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -411,8 +411,8 @@ export default class ChatContextMenu { if(!doc) return false; let hasTarget = !!IS_TOUCH_SUPPORTED; - const isGoodType = !doc.type || !(['gif', 'video', 'sticker'] as MyDocument['type'][]).includes(doc.type); - if(isGoodType) hasTarget = hasTarget || !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio'); + const isGoodType = !doc.type || !(['gif', 'video'/* , 'sticker' */] as MyDocument['type'][]).includes(doc.type); + if(isGoodType) hasTarget ||= !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio') || !!findUpClassName(this.target, 'media-sticker-wrapper'); return isGoodType && hasTarget; } }, { diff --git a/src/components/groupCall/participants.ts b/src/components/groupCall/participants.ts index 142230b2..9f63c47c 100644 --- a/src/components/groupCall/participants.ts +++ b/src/components/groupCall/participants.ts @@ -136,7 +136,7 @@ export class GroupCallParticipantContextMenu { if(this.instance.id === groupCallId) { const peerId = getPeerId(participant.peer); if(this.targetPeerId === peerId) { - contextMenuController.closeBtnMenu(); + contextMenuController.close(); } } }); @@ -147,7 +147,7 @@ export class GroupCallParticipantContextMenu { appendTo = isFull ? PopupElement.getPopups(PopupGroupCall)[0].getContainer(): document.body; if(!isFull) { - contextMenuController.closeBtnMenu(); + contextMenuController.close(); } }, listenerSetter); } diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 3b072151..1dfe5a72 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -240,7 +240,7 @@ export class AppSidebarLeft extends SidebarSlider { btnMenuFooter.classList.add('btn-menu-footer'); btnMenuFooter.addEventListener(CLICK_EVENT_NAME, (e) => { e.stopPropagation(); - contextMenuController.closeBtnMenu(); + contextMenuController.close(); }); const t = document.createElement('span'); t.classList.add('btn-menu-footer-text'); diff --git a/src/components/toast.ts b/src/components/toast.ts index e5c3c522..eafa3c96 100644 --- a/src/components/toast.ts +++ b/src/components/toast.ts @@ -5,24 +5,58 @@ */ import replaceContent from '../helpers/dom/replaceContent'; +import OverlayClickHandler from '../helpers/overlayClickHandler'; import {FormatterArguments, i18n, LangPackKey} from '../lib/langPack'; const toastEl = document.createElement('div'); toastEl.classList.add('toast'); -export function toast(content: string | Node) { - replaceContent(toastEl, content); - document.body.append(toastEl); +let timeout: number; + +const x = new OverlayClickHandler('toast'); +x.addEventListener('toggle', (open) => { + if(!open) { + hideToast(); + } +}); + +export function hideToast() { + x.close(); + + toastEl.classList.remove('is-visible'); + timeout && clearTimeout(+timeout); - if(toastEl.dataset.timeout) clearTimeout(+toastEl.dataset.timeout); - toastEl.dataset.timeout = '' + setTimeout(() => { + timeout = window.setTimeout(() => { toastEl.remove(); - delete toastEl.dataset.timeout; - }, 3000); + timeout = undefined; + }, 200); +} + +export function toast(content: string | Node, onClose?: () => void) { + x.close(); + + replaceContent(toastEl, content); + + if(!toastEl.parentElement) { + document.body.append(toastEl); + void toastEl.offsetLeft; // reflow + } + + toastEl.classList.add('is-visible'); + + timeout && clearTimeout(+timeout); + x.open(toastEl); + + timeout = window.setTimeout(hideToast, 3000); + + if(onClose) { + x.addEventListener('toggle', onClose, {once: true}); + } } export function toastNew(options: Partial<{ langPackKey: LangPackKey, - langPackArguments: FormatterArguments + langPackArguments: FormatterArguments, + onClose: () => void }>) { - toast(i18n(options.langPackKey, options.langPackArguments)); + toast(i18n(options.langPackKey, options.langPackArguments), options.onClose); } diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index 986d0951..f5a2f19f 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -19,13 +19,14 @@ import onMediaLoad from '../../helpers/onMediaLoad'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import throttle from '../../helpers/schedulers/throttle'; import sequentialDom from '../../helpers/sequentialDom'; -import {PhotoSize} from '../../layer'; +import {PhotoSize, VideoSize} from '../../layer'; import {MyDocument} from '../../lib/appManagers/appDocsManager'; import appDownloadManager from '../../lib/appManagers/appDownloadManager'; import appImManager from '../../lib/appManagers/appImManager'; import {AppManagers} from '../../lib/appManagers/managers'; import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId'; import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize'; +import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb'; import lottieLoader from '../../lib/rlottie/lottieLoader'; import RLottiePlayer from '../../lib/rlottie/rlottiePlayer'; import rootScope from '../../lib/rootScope'; @@ -35,9 +36,15 @@ import {SendMessageEmojiInteractionData} from '../../types'; import {getEmojiToneIndex} from '../../vendor/emoji'; import animationIntersector from '../animationIntersector'; import LazyLoadQueue from '../lazyLoadQueue'; +import PopupStickers from '../popups/stickers'; +import {hideToast, toastNew} from '../toast'; import wrapStickerAnimation from './stickerAnimation'; -export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio, static: asStatic, managers = rootScope.managers}: { +// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L40 +const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2; +const EMOJI_EFFECT_MULTIPLIER = 3; + +export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio, static: asStatic, managers = rootScope.managers, fullThumb, isOut}: { doc: MyDocument, div: HTMLElement, middleware?: () => boolean, @@ -55,7 +62,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, needUpscale?: boolean, skipRatio?: number, static?: boolean, - managers?: AppManagers + managers?: AppManagers, + fullThumb?: PhotoSize | VideoSize, + isOut?: boolean }) { const stickerType = doc.sticker; if(stickerType === 1) { @@ -118,17 +127,23 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, return cacheContext = await managers.thumbsStorage.getCacheContext(doc, type); }; + const isAnimated = !asStatic && (stickerType === 2 || stickerType === 3); + + const effectThumb = getStickerEffectThumb(doc); + if(isOut !== undefined && effectThumb && !isOut) { + div.classList.add('reflect-x'); + } + if(asStatic && stickerType !== 1) { const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize; await getCacheContext(thumb.type); } else { - await getCacheContext(); + await getCacheContext(fullThumb?.type); } const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1; const downloaded = cacheContext.downloaded && !needFadeIn; - const isAnimated = !asStatic && (stickerType === 2 || stickerType === 3); const isThumbNeededForType = isAnimated; const lottieCachedThumb = stickerType === 2 || stickerType === 3 ? await managers.appDocsManager.getLottieCachedThumb(doc.id, toneIndex) : undefined; @@ -293,7 +308,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, // appDocsManager.downloadDocNew(doc.id).promise.then((res) => res.json()).then(async(json) => { // fetch(doc.url).then((res) => res.json()).then(async(json) => { - return await appDownloadManager.downloadMedia({media: doc, queueId: lazyLoadQueue?.queueId}) + return await appDownloadManager.downloadMedia({media: doc, queueId: lazyLoadQueue?.queueId, thumb: fullThumb}) .then(async(blob) => { // console.timeEnd('download sticker' + doc.id); // console.log('loaded sticker:', doc, div/* , blob */); @@ -406,24 +421,18 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, return; } - const bubble = findUpClassName(div, 'bubble'); - const isOut = bubble.classList.contains('is-out'); - const {animationDiv} = wrapStickerAnimation({ doc, middleware, side: isOut ? 'right' : 'left', size: 280, target: div, - play: true + play: true, + withRandomOffset: true }); - if(bubble) { - if(isOut) { - animationDiv.classList.add('is-out'); - } else { - animationDiv.classList.add('is-in'); - } + if(isOut !== undefined && !isOut) { + animationDiv.classList.add('reflect-x'); } if(!sendInteractionThrottled) { @@ -464,6 +473,54 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, sendInteractionThrottled(); } }); + } else if(effectThumb && isOut !== undefined) { + managers.appStickersManager.preloadSticker(doc.id, true); + + let playing = false; + attachClickEvent(div, async(e) => { + cancelEvent(e); + if(playing) { + const a = document.createElement('a'); + a.onclick = () => { + hideToast(); + new PopupStickers(doc.stickerSetInput).show(); + }; + + toastNew({ + langPackKey: 'Sticker.Premium.Click.Info', + langPackArguments: [a] + }); + + return; + } + + playing = true; + + const {animationDiv, stickerPromise} = wrapStickerAnimation({ + doc, + middleware, + side: isOut ? 'right' : 'left', + size: width * STICKER_EFFECT_MULTIPLIER, + target: div, + play: true, + fullThumb: effectThumb + }); + + if(isOut !== undefined && !isOut) { + animationDiv.classList.add('reflect-x'); + } + + stickerPromise.then((player) => { + const onFrame = (frameNo: number) => { + if(frameNo === player.maxFrame) { + playing = false; + player.removeEventListener('enterFrame', onFrame); + } + }; + + player.addEventListener('enterFrame', onFrame); + }); + }); } return animation; diff --git a/src/components/wrappers/stickerAnimation.ts b/src/components/wrappers/stickerAnimation.ts index 1ed3abe4..803f5492 100644 --- a/src/components/wrappers/stickerAnimation.ts +++ b/src/components/wrappers/stickerAnimation.ts @@ -8,6 +8,7 @@ import IS_VIBRATE_SUPPORTED from '../../environment/vibrateSupport'; import assumeType from '../../helpers/assumeType'; import isInDOM from '../../helpers/dom/isInDOM'; import throttleWithRaf from '../../helpers/schedulers/throttleWithRaf'; +import {PhotoSize, VideoSize} from '../../layer'; import {MyDocument} from '../../lib/appManagers/appDocsManager'; import appImManager from '../../lib/appManagers/appImManager'; import {AppManagers} from '../../lib/appManagers/managers'; @@ -22,7 +23,9 @@ export default function wrapStickerAnimation({ side, skipRatio, play, - managers + managers, + fullThumb, + withRandomOffset }: { size: number, doc: MyDocument, @@ -31,7 +34,9 @@ export default function wrapStickerAnimation({ side: 'left' | 'center' | 'right', skipRatio?: number, play: boolean, - managers?: AppManagers + managers?: AppManagers, + fullThumb?: PhotoSize | VideoSize, + withRandomOffset?: boolean }) { const animationDiv = document.createElement('div'); animationDiv.classList.add('emoji-animation'); @@ -52,7 +57,8 @@ export default function wrapStickerAnimation({ play, group: 'none', skipRatio, - managers + managers, + fullThumb }).then(({render}) => render).then((animation) => { assumeType(animation); animation.addEventListener('enterFrame', (frameNo) => { @@ -77,9 +83,9 @@ export default function wrapStickerAnimation({ return r > max ? -r % max : r; }; - const randomOffsetX = generateRandomSigned(16); - const randomOffsetY = generateRandomSigned(4); - const stableOffsetX = size / 8 * (side === 'right' ? 1 : -1); + const randomOffsetX = withRandomOffset ? generateRandomSigned(16) : 0; + const randomOffsetY = withRandomOffset ? generateRandomSigned(4) : 0; + const stableOffsetX = /* size / 8 */16 * (side === 'right' ? 1 : -1); const setPosition = () => { if(!isInDOM(target)) { return; diff --git a/src/helpers/contextMenuController.ts b/src/helpers/contextMenuController.ts index 44ee6a4a..9f0a1161 100644 --- a/src/helpers/contextMenuController.ts +++ b/src/helpers/contextMenuController.ts @@ -4,27 +4,17 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import appNavigationController from '../components/appNavigationController'; import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; -import {IS_MOBILE_SAFARI} from '../environment/userAgent'; -import cancelEvent from './dom/cancelEvent'; -import {CLICK_EVENT_NAME} from './dom/clickEvent'; -import EventListenerBase from './eventListenerBase'; import mediaSizes from './mediaSizes'; +import OverlayClickHandler from './overlayClickHandler'; -class ContextMenuController extends EventListenerBase<{ - toggle: (open: boolean) => void -}> { - private openedMenu: HTMLElement; - private menuOverlay: HTMLElement; - private openedMenuOnClose: () => void; - +class ContextMenuController extends OverlayClickHandler { constructor() { - super(); + super('menu', true); mediaSizes.addEventListener('resize', () => { - if(this.openedMenu) { - this.closeBtnMenu(); + if(this.element) { + this.close(); } /* if(openedMenu && (openedMenu.style.top || openedMenu.style.left)) { @@ -33,118 +23,53 @@ class ContextMenuController extends EventListenerBase<{ console.log(innerWidth, innerHeight, rect); } */ - }) + }); } public isOpened() { - return !!this.openedMenu; + return !!this.element; } private onMouseMove = (e: MouseEvent) => { - const rect = this.openedMenu.getBoundingClientRect(); + const rect = this.element.getBoundingClientRect(); const {clientX, clientY} = e; const diffX = clientX >= rect.right ? clientX - rect.right : rect.left - clientX; const diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY; if(diffX >= 100 || diffY >= 100) { - this.closeBtnMenu(); + this.close(); // openedMenu.parentElement.click(); } // console.log('mousemove', diffX, diffY); }; - private onClick = (e: MouseEvent | TouchEvent) => { - // cancelEvent(e); - this.closeBtnMenu(); - }; - - // ! no need in this due to the same handler in appNavigationController - /* const onKeyDown = (e: KeyboardEvent) => { - if(e.key === 'Escape') { - closeBtnMenu(); - cancelEvent(e); + public close() { + if(this.element) { + this.element.classList.remove('active'); + this.element.parentElement.classList.remove('menu-open'); } - }; */ - public closeBtnMenu = () => { - if(this.openedMenu) { - this.openedMenu.classList.remove('active'); - this.openedMenu.parentElement.classList.remove('menu-open'); - // openedMenu.previousElementSibling.remove(); // remove overlay - if(this.menuOverlay) this.menuOverlay.remove(); - this.openedMenu = undefined; - - this.dispatchEvent('toggle', false); - } - - if(this.openedMenuOnClose) { - this.openedMenuOnClose(); - this.openedMenuOnClose = undefined; - } + super.close(); if(!IS_TOUCH_SUPPORTED) { window.removeEventListener('mousemove', this.onMouseMove); - // window.removeEventListener('keydown', onKeyDown, {capture: true}); - window.removeEventListener('contextmenu', this.onClick); - } - - document.removeEventListener(CLICK_EVENT_NAME, this.onClick); - - if(!IS_MOBILE_SAFARI) { - appNavigationController.removeByType('menu'); - } - }; - - public openBtnMenu(menuElement: HTMLElement, onClose?: () => void) { - this.closeBtnMenu(); - - if(!IS_MOBILE_SAFARI) { - appNavigationController.pushItem({ - type: 'menu', - onPop: (canAnimate) => { - this.closeBtnMenu(); - } - }); } + } - this.openedMenu = menuElement; - this.openedMenu.classList.add('active'); - this.openedMenu.parentElement.classList.add('menu-open'); + public openBtnMenu(element: HTMLElement, onClose?: () => void) { + super.open(element); - if(!this.menuOverlay) { - this.menuOverlay = document.createElement('div'); - this.menuOverlay.classList.add('btn-menu-overlay'); + this.element.classList.add('active'); + this.element.parentElement.classList.add('menu-open'); - // ! because this event must be canceled, and can't cancel on menu click (below) - this.menuOverlay.addEventListener(CLICK_EVENT_NAME, (e) => { - cancelEvent(e); - this.onClick(e); - }); + if(onClose) { + this.addEventListener('toggle', onClose, {once: true}); } - this.openedMenu.parentElement.insertBefore(this.menuOverlay, this.openedMenu); - - // document.body.classList.add('disable-hover'); - - this.openedMenuOnClose = onClose; - if(!IS_TOUCH_SUPPORTED) { window.addEventListener('mousemove', this.onMouseMove); - // window.addEventListener('keydown', onKeyDown, {capture: true}); - window.addEventListener('contextmenu', this.onClick, {once: true}); } - - /* // ! because this event must be canceled, and can't cancel on menu click (below) - overlay.addEventListener(CLICK_EVENT_NAME, (e) => { - cancelEvent(e); - onClick(e); - }); */ - - // ! safari iOS doesn't handle window click event on overlay, idk why - document.addEventListener(CLICK_EVENT_NAME, this.onClick); - - this.dispatchEvent('toggle', true); } } diff --git a/src/helpers/dom/clickEvent.ts b/src/helpers/dom/clickEvent.ts index fdeab575..4194d944 100644 --- a/src/helpers/dom/clickEvent.ts +++ b/src/helpers/dom/clickEvent.ts @@ -45,11 +45,11 @@ export function attachClickEvent(elem: HTMLElement | Window, callback: (e: /* To add(CLICK_EVENT_NAME, callback, options); } -export function detachClickEvent(elem: HTMLElement, callback: (e: /* TouchEvent | */MouseEvent) => void, options?: AddEventListenerOptions) { +export function detachClickEvent(elem: HTMLElement | Window, callback: (e: /* TouchEvent | */MouseEvent) => void, options?: AddEventListenerOptions) { // if(CLICK_EVENT_NAME === 'touchend') { // elem.removeEventListener('touchstart', callback, options); // } else { - elem.removeEventListener(CLICK_EVENT_NAME, callback, options); + elem.removeEventListener(CLICK_EVENT_NAME, callback as any, options); // } } diff --git a/src/helpers/overlayClickHandler.ts b/src/helpers/overlayClickHandler.ts new file mode 100644 index 00000000..0dda60ec --- /dev/null +++ b/src/helpers/overlayClickHandler.ts @@ -0,0 +1,103 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import appNavigationController, {NavigationItem} from '../components/appNavigationController'; +import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; +import {IS_MOBILE_SAFARI} from '../environment/userAgent'; +import cancelEvent from './dom/cancelEvent'; +import {CLICK_EVENT_NAME} from './dom/clickEvent'; +import findUpAsChild from './dom/findUpAsChild'; +import EventListenerBase from './eventListenerBase'; + +export default class OverlayClickHandler extends EventListenerBase<{ + toggle: (open: boolean) => void +}> { + protected element: HTMLElement; + protected overlay: HTMLElement; + protected listenerOptions: AddEventListenerOptions; + + constructor( + protected navigationType: NavigationItem['type'], + protected withOverlay?: boolean + ) { + super(false); + this.listenerOptions = withOverlay ? undefined : {capture: true}; + } + + protected onClick = (e: MouseEvent | TouchEvent) => { + if(this.element && findUpAsChild(e.target, this.element)) { + return; + } + + cancelEvent(e); + this.close(); + }; + + public close() { + if(this.element) { + this.overlay?.remove(); + this.element = undefined; + this.dispatchEvent('toggle', false); + } + + if(!IS_TOUCH_SUPPORTED) { + // window.removeEventListener('keydown', onKeyDown, {capture: true}); + window.removeEventListener('contextmenu', this.onClick); + } + + document.removeEventListener(CLICK_EVENT_NAME, this.onClick, this.listenerOptions); + + if(!IS_MOBILE_SAFARI) { + appNavigationController.removeByType(this.navigationType); + } + } + + public open(element: HTMLElement) { + this.close(); + + if(!IS_MOBILE_SAFARI) { + appNavigationController.pushItem({ + type: this.navigationType, + onPop: (canAnimate) => { + this.close(); + } + }); + } + + this.element = element; + + if(!this.overlay && this.withOverlay) { + this.overlay = document.createElement('div'); + this.overlay.classList.add('btn-menu-overlay'); + + // ! because this event must be canceled, and can't cancel on menu click (below) + this.overlay.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); + this.onClick(e); + }); + } + + this.overlay && this.element.parentElement.insertBefore(this.overlay, this.element); + + // document.body.classList.add('disable-hover'); + + if(!IS_TOUCH_SUPPORTED) { + // window.addEventListener('keydown', onKeyDown, {capture: true}); + window.addEventListener('contextmenu', this.onClick, {once: true}); + } + + /* // ! because this event must be canceled, and can't cancel on menu click (below) + overlay.addEventListener(CLICK_EVENT_NAME, (e) => { + cancelEvent(e); + onClick(e); + }); */ + + // ! safari iOS doesn't handle window click event on overlay, idk why + document.addEventListener(CLICK_EVENT_NAME, this.onClick, this.listenerOptions); + + this.dispatchEvent('toggle', true); + } +} diff --git a/src/lang.ts b/src/lang.ts index d086ddf8..ad8c9f76 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -1119,6 +1119,7 @@ const lang = { 'Schedule.SendToday': 'Send today at %@', 'Schedule.SendDate': 'Send on %@ at %@', 'Schedule.SendWhenOnline': 'Send When Online', + 'Sticker.Premium.Click.Info': 'This pack contains premium stickers like this one. [View Pack]()', 'Stickers.Recent': 'Recent', // "Stickers.Favorite": "Favorite", 'StickerSet.DontExist': 'Sorry, this sticker set doesn\'t seem to exist.', diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 6f8944fe..ee9a296c 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -249,7 +249,7 @@ export class AppStickersManager extends AppManager { const id = isAnimation ? EMOJI_ANIMATIONS_SET_LOCAL_ID : EMOJI_SET_LOCAL_ID; const stickerSet = this.storage.getFromCache(id); // const stickerSet = await this.getStickerSet({id}); - if(!stickerSet || !stickerSet.documents) return; + if(!stickerSet?.documents) return; if(isAnimation) { if(['๐Ÿงก', '๐Ÿ’›', '๐Ÿ’š', '๐Ÿ’™', '๐Ÿ’œ', '๐Ÿ–ค', '๐Ÿค', '๐ŸคŽ'].includes(emoji)) { @@ -293,14 +293,19 @@ export class AppStickersManager extends AppManager { const sound = this.getAnimatedEmojiSoundDocument(emoji); return Promise.all([ - this.apiFileManager.downloadMedia({media: doc}), - sound ? this.apiFileManager.downloadMedia({media: sound}) : undefined + this.preloadSticker(doc.id), + sound ? this.preloadSticker(sound.id) : undefined ]).then(() => { return {doc, sound}; }); }); } + public preloadSticker(docId: DocId, isPremiumEffect?: boolean) { + const doc = this.appDocsManager.getDoc(docId); + return this.apiFileManager.downloadMedia({media: doc, thumb: isPremiumEffect ? doc.video_thumbs?.[0] : undefined}); + } + private saveStickerSet(res: Omit, id: DocId) { const newSet: MessagesStickerSet = { _: 'messages.stickerSet', diff --git a/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts b/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts index b75e4d53..a6f1672c 100644 --- a/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts +++ b/src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts @@ -4,15 +4,15 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import type {Document, PhotoSize} from '../../../../layer'; +import type {Document, PhotoSize, VideoSize} from '../../../../layer'; import type {DownloadOptions} from '../../../mtproto/apiFileManager'; import getDocumentInput from './getDocumentInput'; -export default function getDocumentDownloadOptions(doc: Document.document, thumb?: PhotoSize.photoSize, queueId?: number, onlyCache?: boolean): DownloadOptions { +export default function getDocumentDownloadOptions(doc: Document.document, thumb?: PhotoSize.photoSize | VideoSize, queueId?: number, onlyCache?: boolean): DownloadOptions { const inputFileLocation = getDocumentInput(doc, thumb?.type); let mimeType: string; - if(thumb) { + if(thumb?._ === 'photoSize') { mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */; } else { mimeType = doc.mime_type || 'application/octet-stream'; diff --git a/src/lib/appManagers/utils/download/getDownloadMediaDetails.ts b/src/lib/appManagers/utils/download/getDownloadMediaDetails.ts index 6c815c17..022e2860 100644 --- a/src/lib/appManagers/utils/download/getDownloadMediaDetails.ts +++ b/src/lib/appManagers/utils/download/getDownloadMediaDetails.ts @@ -16,7 +16,7 @@ export default function getDownloadMediaDetails(options: DownloadMediaOptions) { let downloadOptions: DownloadOptions; if(media._ === 'document') downloadOptions = getDocumentDownloadOptions(media, thumb as any, queueId, onlyCache); - else if(media._ === 'photo') downloadOptions = getPhotoDownloadOptions(media, thumb, queueId, onlyCache); + else if(media._ === 'photo') downloadOptions = getPhotoDownloadOptions(media, thumb as any, queueId, onlyCache); else if(isWebDocument(media)) downloadOptions = getWebDocumentDownloadOptions(media); downloadOptions.downloadId = options.downloadId; diff --git a/src/lib/appManagers/utils/stickers/getStickerEffectThumb.ts b/src/lib/appManagers/utils/stickers/getStickerEffectThumb.ts new file mode 100644 index 00000000..7bad6f92 --- /dev/null +++ b/src/lib/appManagers/utils/stickers/getStickerEffectThumb.ts @@ -0,0 +1,5 @@ +import {MyDocument} from '../../appDocsManager'; + +export default function getStickerEffectThumb(doc: MyDocument) { + return doc.video_thumbs?.[0]; +} diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index d85b9930..e461b202 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -13,7 +13,7 @@ import type {ReferenceBytes} from './referenceDatabase'; import Modes from '../../config/modes'; import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; import {randomLong} from '../../helpers/random'; -import {Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile, WebDocument} from '../../layer'; +import {Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile, VideoSize, WebDocument} from '../../layer'; import {DcId} from '../../types'; import CacheStorageController from '../files/cacheStorage'; import {logger, LogTypes} from '../logger'; @@ -62,7 +62,7 @@ export type DownloadOptions = { export type DownloadMediaOptions = { media: Photo.photo | Document.document | WebDocument, - thumb?: PhotoSize, + thumb?: PhotoSize | VideoSize, queueId?: number, onlyCache?: boolean, downloadId?: string diff --git a/src/scss/components/_global.scss b/src/scss/components/_global.scss index 835eb861..6cf61494 100644 --- a/src/scss/components/_global.scss +++ b/src/scss/components/_global.scss @@ -177,6 +177,10 @@ Utility Classes pointer-events: none !important; } +.reflect-x { + transform: scaleX(-1); +} + /* .flex-grow { flex-grow: 1; } diff --git a/src/scss/partials/_emojiAnimation.scss b/src/scss/partials/_emojiAnimation.scss index f978f002..442860dc 100644 --- a/src/scss/partials/_emojiAnimation.scss +++ b/src/scss/partials/_emojiAnimation.scss @@ -10,12 +10,6 @@ // @include sidebar-transform(true); - &.is-in { - .rlottie { - transform: scaleX(-1); - } - } - &-container { position: absolute; top: 0; diff --git a/src/scss/style.scss b/src/scss/style.scss index 7f4b33e5..f0727dff 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -699,13 +699,27 @@ input:-webkit-autofill:active { color: #fff; font-size: 1rem; border-radius: $border-radius-medium; - animation: fade-in-opacity-fade-out-opacity 3s linear forwards; z-index: 5; - max-width: 22.5rem; + max-width: unquote('min(30rem, calc(100vw - 2rem))'); + opacity: 0; + backdrop-filter: blur(25px); + + &.is-visible { + opacity: 1; + } + + @include animation-level(2) { + transition: opacity var(--transition-standard-in); + } b { color: inherit; } + + a { + color: #60a5e9!important; + cursor: pointer; + } } hr {