Browse Source

Stickers: premium effect & toast

master
Eduard Kuzmenko 2 years ago
parent
commit
0c4a99f67d
  1. 3
      src/components/appNavigationController.ts
  2. 2
      src/components/buttonMenu.ts
  3. 2
      src/components/buttonMenuToggle.ts
  4. 3
      src/components/chat/bubbles.ts
  5. 4
      src/components/chat/chat.ts
  6. 4
      src/components/chat/contextMenu.ts
  7. 4
      src/components/groupCall/participants.ts
  8. 2
      src/components/sidebarLeft/index.ts
  9. 50
      src/components/toast.ts
  10. 89
      src/components/wrappers/sticker.ts
  11. 18
      src/components/wrappers/stickerAnimation.ts
  12. 115
      src/helpers/contextMenuController.ts
  13. 4
      src/helpers/dom/clickEvent.ts
  14. 103
      src/helpers/overlayClickHandler.ts
  15. 1
      src/lang.ts
  16. 11
      src/lib/appManagers/appStickersManager.ts
  17. 6
      src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts
  18. 2
      src/lib/appManagers/utils/download/getDownloadMediaDetails.ts
  19. 5
      src/lib/appManagers/utils/stickers/getStickerEffectThumb.ts
  20. 4
      src/lib/mtproto/apiFileManager.ts
  21. 4
      src/scss/components/_global.scss
  22. 6
      src/scss/partials/_emojiAnimation.scss
  23. 18
      src/scss/style.scss

3
src/components/appNavigationController.ts

@ -15,7 +15,8 @@ import indexOfAndSplice from '../helpers/array/indexOfAndSplice';
export type NavigationItem = { export type NavigationItem = {
type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' | type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' |
'esg' | 'multiselect' | 'input-helper' | 'autocomplete-helper' | 'markup' | '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, onPop: (canAnimate: boolean) => boolean | void,
onEscape?: () => boolean, onEscape?: () => boolean,
noHistory?: boolean, noHistory?: boolean,

2
src/components/buttonMenu.ts

@ -62,7 +62,7 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
} }
if(!keepOpen) { if(!keepOpen) {
contextMenuController.closeBtnMenu(); contextMenuController.close();
} }
if(checkboxField && !noCheckboxClickListener/* && result !== false */) { if(checkboxField && !noCheckboxClickListener/* && result !== false */) {

2
src/components/buttonMenuToggle.ts

@ -49,7 +49,7 @@ const ButtonMenuToggleHandler = (el: HTMLElement, onOpen?: (e: Event) => void |
cancelEvent(e); cancelEvent(e);
if(el.classList.contains('menu-open')) { if(el.classList.contains('menu-open')) {
contextMenuController.closeBtnMenu(); contextMenuController.close();
} else { } else {
const result = onOpen && onOpen(e); const result = onOpen && onOpen(e);
const open = () => { const open = () => {

3
src/components/chat/bubbles.ts

@ -4016,7 +4016,8 @@ export default class ChatBubbles {
loop: true, loop: true,
emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined, emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined,
withThumb: true, withThumb: true,
loadPromises loadPromises,
isOut
}); });
} else if(doc.type === 'video' || doc.type === 'gif' || doc.type === 'round'/* && doc.size <= 20e6 */) { } else if(doc.type === 'video' || doc.type === 'gif' || doc.type === 'round'/* && doc.size <= 20e6 */) {
// this.log('never get free 2', doc); // this.log('never get free 2', doc);

4
src/components/chat/chat.ts

@ -605,13 +605,13 @@ export default class Chat extends EventListenerBase<{
} }
public isOurMessage(message: Message.message | Message.messageService) { 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) { public isOutMessage(message: Message.message | Message.messageService) {
const fwdFrom = (message as Message.message).fwd_from; const fwdFrom = (message as Message.message).fwd_from;
const isOut = this.isOurMessage(message) && (!fwdFrom || this.peerId !== rootScope.myId); const isOut = this.isOurMessage(message) && (!fwdFrom || this.peerId !== rootScope.myId);
return isOut; return !!isOut;
} }
public isAvatarNeeded(message: Message.message | Message.messageService) { public isAvatarNeeded(message: Message.message | Message.messageService) {

4
src/components/chat/contextMenu.ts

@ -411,8 +411,8 @@ export default class ChatContextMenu {
if(!doc) return false; if(!doc) return false;
let hasTarget = !!IS_TOUCH_SUPPORTED; let hasTarget = !!IS_TOUCH_SUPPORTED;
const isGoodType = !doc.type || !(['gif', 'video', 'sticker'] as MyDocument['type'][]).includes(doc.type); 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'); if(isGoodType) hasTarget ||= !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio') || !!findUpClassName(this.target, 'media-sticker-wrapper');
return isGoodType && hasTarget; return isGoodType && hasTarget;
} }
}, { }, {

4
src/components/groupCall/participants.ts

@ -136,7 +136,7 @@ export class GroupCallParticipantContextMenu {
if(this.instance.id === groupCallId) { if(this.instance.id === groupCallId) {
const peerId = getPeerId(participant.peer); const peerId = getPeerId(participant.peer);
if(this.targetPeerId === peerId) { if(this.targetPeerId === peerId) {
contextMenuController.closeBtnMenu(); contextMenuController.close();
} }
} }
}); });
@ -147,7 +147,7 @@ export class GroupCallParticipantContextMenu {
appendTo = isFull ? PopupElement.getPopups(PopupGroupCall)[0].getContainer(): document.body; appendTo = isFull ? PopupElement.getPopups(PopupGroupCall)[0].getContainer(): document.body;
if(!isFull) { if(!isFull) {
contextMenuController.closeBtnMenu(); contextMenuController.close();
} }
}, listenerSetter); }, listenerSetter);
} }

2
src/components/sidebarLeft/index.ts

@ -240,7 +240,7 @@ export class AppSidebarLeft extends SidebarSlider {
btnMenuFooter.classList.add('btn-menu-footer'); btnMenuFooter.classList.add('btn-menu-footer');
btnMenuFooter.addEventListener(CLICK_EVENT_NAME, (e) => { btnMenuFooter.addEventListener(CLICK_EVENT_NAME, (e) => {
e.stopPropagation(); e.stopPropagation();
contextMenuController.closeBtnMenu(); contextMenuController.close();
}); });
const t = document.createElement('span'); const t = document.createElement('span');
t.classList.add('btn-menu-footer-text'); t.classList.add('btn-menu-footer-text');

50
src/components/toast.ts

@ -5,24 +5,58 @@
*/ */
import replaceContent from '../helpers/dom/replaceContent'; import replaceContent from '../helpers/dom/replaceContent';
import OverlayClickHandler from '../helpers/overlayClickHandler';
import {FormatterArguments, i18n, LangPackKey} from '../lib/langPack'; import {FormatterArguments, i18n, LangPackKey} from '../lib/langPack';
const toastEl = document.createElement('div'); const toastEl = document.createElement('div');
toastEl.classList.add('toast'); toastEl.classList.add('toast');
export function toast(content: string | Node) { 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);
timeout = window.setTimeout(() => {
toastEl.remove();
timeout = undefined;
}, 200);
}
export function toast(content: string | Node, onClose?: () => void) {
x.close();
replaceContent(toastEl, content); replaceContent(toastEl, content);
if(!toastEl.parentElement) {
document.body.append(toastEl); document.body.append(toastEl);
void toastEl.offsetLeft; // reflow
}
if(toastEl.dataset.timeout) clearTimeout(+toastEl.dataset.timeout); toastEl.classList.add('is-visible');
toastEl.dataset.timeout = '' + setTimeout(() => {
toastEl.remove(); timeout && clearTimeout(+timeout);
delete toastEl.dataset.timeout; x.open(toastEl);
}, 3000);
timeout = window.setTimeout(hideToast, 3000);
if(onClose) {
x.addEventListener('toggle', onClose, {once: true});
}
} }
export function toastNew(options: Partial<{ export function toastNew(options: Partial<{
langPackKey: LangPackKey, langPackKey: LangPackKey,
langPackArguments: FormatterArguments langPackArguments: FormatterArguments,
onClose: () => void
}>) { }>) {
toast(i18n(options.langPackKey, options.langPackArguments)); toast(i18n(options.langPackKey, options.langPackArguments), options.onClose);
} }

89
src/components/wrappers/sticker.ts

@ -19,13 +19,14 @@ import onMediaLoad from '../../helpers/onMediaLoad';
import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview'; import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview';
import throttle from '../../helpers/schedulers/throttle'; import throttle from '../../helpers/schedulers/throttle';
import sequentialDom from '../../helpers/sequentialDom'; import sequentialDom from '../../helpers/sequentialDom';
import {PhotoSize} from '../../layer'; import {PhotoSize, VideoSize} from '../../layer';
import {MyDocument} from '../../lib/appManagers/appDocsManager'; import {MyDocument} from '../../lib/appManagers/appDocsManager';
import appDownloadManager from '../../lib/appManagers/appDownloadManager'; import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import appImManager from '../../lib/appManagers/appImManager'; import appImManager from '../../lib/appManagers/appImManager';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId'; import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId';
import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize'; import choosePhotoSize from '../../lib/appManagers/utils/photos/choosePhotoSize';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import lottieLoader from '../../lib/rlottie/lottieLoader'; import lottieLoader from '../../lib/rlottie/lottieLoader';
import RLottiePlayer from '../../lib/rlottie/rlottiePlayer'; import RLottiePlayer from '../../lib/rlottie/rlottiePlayer';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
@ -35,9 +36,15 @@ import {SendMessageEmojiInteractionData} from '../../types';
import {getEmojiToneIndex} from '../../vendor/emoji'; import {getEmojiToneIndex} from '../../vendor/emoji';
import animationIntersector from '../animationIntersector'; import animationIntersector from '../animationIntersector';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import PopupStickers from '../popups/stickers';
import {hideToast, toastNew} from '../toast';
import wrapStickerAnimation from './stickerAnimation'; 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, doc: MyDocument,
div: HTMLElement, div: HTMLElement,
middleware?: () => boolean, middleware?: () => boolean,
@ -55,7 +62,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
needUpscale?: boolean, needUpscale?: boolean,
skipRatio?: number, skipRatio?: number,
static?: boolean, static?: boolean,
managers?: AppManagers managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize,
isOut?: boolean
}) { }) {
const stickerType = doc.sticker; const stickerType = doc.sticker;
if(stickerType === 1) { if(stickerType === 1) {
@ -118,17 +127,23 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
return cacheContext = await managers.thumbsStorage.getCacheContext(doc, type); 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) { if(asStatic && stickerType !== 1) {
const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize; const thumb = choosePhotoSize(doc, width, height, false) as PhotoSize.photoSize;
await getCacheContext(thumb.type); await getCacheContext(thumb.type);
} else { } else {
await getCacheContext(); await getCacheContext(fullThumb?.type);
} }
const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1; const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1;
const downloaded = cacheContext.downloaded && !needFadeIn; const downloaded = cacheContext.downloaded && !needFadeIn;
const isAnimated = !asStatic && (stickerType === 2 || stickerType === 3);
const isThumbNeededForType = isAnimated; const isThumbNeededForType = isAnimated;
const lottieCachedThumb = stickerType === 2 || stickerType === 3 ? await managers.appDocsManager.getLottieCachedThumb(doc.id, toneIndex) : undefined; 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) => { // appDocsManager.downloadDocNew(doc.id).promise.then((res) => res.json()).then(async(json) => {
// fetch(doc.url).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) => { .then(async(blob) => {
// console.timeEnd('download sticker' + doc.id); // console.timeEnd('download sticker' + doc.id);
// console.log('loaded sticker:', doc, div/* , blob */); // console.log('loaded sticker:', doc, div/* , blob */);
@ -406,24 +421,18 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
return; return;
} }
const bubble = findUpClassName(div, 'bubble');
const isOut = bubble.classList.contains('is-out');
const {animationDiv} = wrapStickerAnimation({ const {animationDiv} = wrapStickerAnimation({
doc, doc,
middleware, middleware,
side: isOut ? 'right' : 'left', side: isOut ? 'right' : 'left',
size: 280, size: 280,
target: div, target: div,
play: true play: true,
withRandomOffset: true
}); });
if(bubble) { if(isOut !== undefined && !isOut) {
if(isOut) { animationDiv.classList.add('reflect-x');
animationDiv.classList.add('is-out');
} else {
animationDiv.classList.add('is-in');
}
} }
if(!sendInteractionThrottled) { if(!sendInteractionThrottled) {
@ -464,6 +473,54 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
sendInteractionThrottled(); 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; return animation;

18
src/components/wrappers/stickerAnimation.ts

@ -8,6 +8,7 @@ import IS_VIBRATE_SUPPORTED from '../../environment/vibrateSupport';
import assumeType from '../../helpers/assumeType'; import assumeType from '../../helpers/assumeType';
import isInDOM from '../../helpers/dom/isInDOM'; import isInDOM from '../../helpers/dom/isInDOM';
import throttleWithRaf from '../../helpers/schedulers/throttleWithRaf'; import throttleWithRaf from '../../helpers/schedulers/throttleWithRaf';
import {PhotoSize, VideoSize} from '../../layer';
import {MyDocument} from '../../lib/appManagers/appDocsManager'; import {MyDocument} from '../../lib/appManagers/appDocsManager';
import appImManager from '../../lib/appManagers/appImManager'; import appImManager from '../../lib/appManagers/appImManager';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
@ -22,7 +23,9 @@ export default function wrapStickerAnimation({
side, side,
skipRatio, skipRatio,
play, play,
managers managers,
fullThumb,
withRandomOffset
}: { }: {
size: number, size: number,
doc: MyDocument, doc: MyDocument,
@ -31,7 +34,9 @@ export default function wrapStickerAnimation({
side: 'left' | 'center' | 'right', side: 'left' | 'center' | 'right',
skipRatio?: number, skipRatio?: number,
play: boolean, play: boolean,
managers?: AppManagers managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize,
withRandomOffset?: boolean
}) { }) {
const animationDiv = document.createElement('div'); const animationDiv = document.createElement('div');
animationDiv.classList.add('emoji-animation'); animationDiv.classList.add('emoji-animation');
@ -52,7 +57,8 @@ export default function wrapStickerAnimation({
play, play,
group: 'none', group: 'none',
skipRatio, skipRatio,
managers managers,
fullThumb
}).then(({render}) => render).then((animation) => { }).then(({render}) => render).then((animation) => {
assumeType<RLottiePlayer>(animation); assumeType<RLottiePlayer>(animation);
animation.addEventListener('enterFrame', (frameNo) => { animation.addEventListener('enterFrame', (frameNo) => {
@ -77,9 +83,9 @@ export default function wrapStickerAnimation({
return r > max ? -r % max : r; return r > max ? -r % max : r;
}; };
const randomOffsetX = generateRandomSigned(16); const randomOffsetX = withRandomOffset ? generateRandomSigned(16) : 0;
const randomOffsetY = generateRandomSigned(4); const randomOffsetY = withRandomOffset ? generateRandomSigned(4) : 0;
const stableOffsetX = size / 8 * (side === 'right' ? 1 : -1); const stableOffsetX = /* size / 8 */16 * (side === 'right' ? 1 : -1);
const setPosition = () => { const setPosition = () => {
if(!isInDOM(target)) { if(!isInDOM(target)) {
return; return;

115
src/helpers/contextMenuController.ts

@ -4,27 +4,17 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import appNavigationController from '../components/appNavigationController';
import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; 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 mediaSizes from './mediaSizes';
import OverlayClickHandler from './overlayClickHandler';
class ContextMenuController extends EventListenerBase<{ class ContextMenuController extends OverlayClickHandler {
toggle: (open: boolean) => void
}> {
private openedMenu: HTMLElement;
private menuOverlay: HTMLElement;
private openedMenuOnClose: () => void;
constructor() { constructor() {
super(); super('menu', true);
mediaSizes.addEventListener('resize', () => { mediaSizes.addEventListener('resize', () => {
if(this.openedMenu) { if(this.element) {
this.closeBtnMenu(); this.close();
} }
/* if(openedMenu && (openedMenu.style.top || openedMenu.style.left)) { /* if(openedMenu && (openedMenu.style.top || openedMenu.style.left)) {
@ -33,118 +23,53 @@ class ContextMenuController extends EventListenerBase<{
console.log(innerWidth, innerHeight, rect); console.log(innerWidth, innerHeight, rect);
} */ } */
}) });
} }
public isOpened() { public isOpened() {
return !!this.openedMenu; return !!this.element;
} }
private onMouseMove = (e: MouseEvent) => { private onMouseMove = (e: MouseEvent) => {
const rect = this.openedMenu.getBoundingClientRect(); const rect = this.element.getBoundingClientRect();
const {clientX, clientY} = e; const {clientX, clientY} = e;
const diffX = clientX >= rect.right ? clientX - rect.right : rect.left - clientX; const diffX = clientX >= rect.right ? clientX - rect.right : rect.left - clientX;
const diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY; const diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY;
if(diffX >= 100 || diffY >= 100) { if(diffX >= 100 || diffY >= 100) {
this.closeBtnMenu(); this.close();
// openedMenu.parentElement.click(); // openedMenu.parentElement.click();
} }
// console.log('mousemove', diffX, diffY); // console.log('mousemove', diffX, diffY);
}; };
private onClick = (e: MouseEvent | TouchEvent) => { public close() {
// cancelEvent(e); if(this.element) {
this.closeBtnMenu(); this.element.classList.remove('active');
}; this.element.parentElement.classList.remove('menu-open');
// ! no need in this due to the same handler in appNavigationController
/* const onKeyDown = (e: KeyboardEvent) => {
if(e.key === 'Escape') {
closeBtnMenu();
cancelEvent(e);
}
}; */
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) { super.close();
this.openedMenuOnClose();
this.openedMenuOnClose = undefined;
}
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
window.removeEventListener('mousemove', this.onMouseMove); 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) { public openBtnMenu(element: HTMLElement, onClose?: () => void) {
this.closeBtnMenu(); super.open(element);
if(!IS_MOBILE_SAFARI) { this.element.classList.add('active');
appNavigationController.pushItem({ this.element.parentElement.classList.add('menu-open');
type: 'menu',
onPop: (canAnimate) => {
this.closeBtnMenu();
}
});
}
this.openedMenu = menuElement;
this.openedMenu.classList.add('active');
this.openedMenu.parentElement.classList.add('menu-open');
if(!this.menuOverlay) { if(onClose) {
this.menuOverlay = document.createElement('div'); this.addEventListener('toggle', onClose, {once: true});
this.menuOverlay.classList.add('btn-menu-overlay');
// ! 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);
});
} }
this.openedMenu.parentElement.insertBefore(this.menuOverlay, this.openedMenu);
// document.body.classList.add('disable-hover');
this.openedMenuOnClose = onClose;
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
window.addEventListener('mousemove', this.onMouseMove); 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);
} }
} }

4
src/helpers/dom/clickEvent.ts

@ -45,11 +45,11 @@ export function attachClickEvent(elem: HTMLElement | Window, callback: (e: /* To
add(CLICK_EVENT_NAME, callback, options); 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') { // if(CLICK_EVENT_NAME === 'touchend') {
// elem.removeEventListener('touchstart', callback, options); // elem.removeEventListener('touchstart', callback, options);
// } else { // } else {
elem.removeEventListener(CLICK_EVENT_NAME, callback, options); elem.removeEventListener(CLICK_EVENT_NAME, callback as any, options);
// } // }
} }

103
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);
}
}

1
src/lang.ts

@ -1119,6 +1119,7 @@ const lang = {
'Schedule.SendToday': 'Send today at %@', 'Schedule.SendToday': 'Send today at %@',
'Schedule.SendDate': 'Send on %@ at %@', 'Schedule.SendDate': 'Send on %@ at %@',
'Schedule.SendWhenOnline': 'Send When Online', 'Schedule.SendWhenOnline': 'Send When Online',
'Sticker.Premium.Click.Info': 'This pack contains premium stickers like this one. [View Pack]()',
'Stickers.Recent': 'Recent', 'Stickers.Recent': 'Recent',
// "Stickers.Favorite": "Favorite", // "Stickers.Favorite": "Favorite",
'StickerSet.DontExist': 'Sorry, this sticker set doesn\'t seem to exist.', 'StickerSet.DontExist': 'Sorry, this sticker set doesn\'t seem to exist.',

11
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 id = isAnimation ? EMOJI_ANIMATIONS_SET_LOCAL_ID : EMOJI_SET_LOCAL_ID;
const stickerSet = this.storage.getFromCache(id); const stickerSet = this.storage.getFromCache(id);
// const stickerSet = await this.getStickerSet({id}); // const stickerSet = await this.getStickerSet({id});
if(!stickerSet || !stickerSet.documents) return; if(!stickerSet?.documents) return;
if(isAnimation) { if(isAnimation) {
if(['🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎'].includes(emoji)) { if(['🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎'].includes(emoji)) {
@ -293,14 +293,19 @@ export class AppStickersManager extends AppManager {
const sound = this.getAnimatedEmojiSoundDocument(emoji); const sound = this.getAnimatedEmojiSoundDocument(emoji);
return Promise.all([ return Promise.all([
this.apiFileManager.downloadMedia({media: doc}), this.preloadSticker(doc.id),
sound ? this.apiFileManager.downloadMedia({media: sound}) : undefined sound ? this.preloadSticker(sound.id) : undefined
]).then(() => { ]).then(() => {
return {doc, sound}; 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<MessagesStickerSet.messagesStickerSet, '_'>, id: DocId) { private saveStickerSet(res: Omit<MessagesStickerSet.messagesStickerSet, '_'>, id: DocId) {
const newSet: MessagesStickerSet = { const newSet: MessagesStickerSet = {
_: 'messages.stickerSet', _: 'messages.stickerSet',

6
src/lib/appManagers/utils/docs/getDocumentDownloadOptions.ts

@ -4,15 +4,15 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * 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 type {DownloadOptions} from '../../../mtproto/apiFileManager';
import getDocumentInput from './getDocumentInput'; 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); const inputFileLocation = getDocumentInput(doc, thumb?.type);
let mimeType: string; let mimeType: string;
if(thumb) { if(thumb?._ === 'photoSize') {
mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */; mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */;
} else { } else {
mimeType = doc.mime_type || 'application/octet-stream'; mimeType = doc.mime_type || 'application/octet-stream';

2
src/lib/appManagers/utils/download/getDownloadMediaDetails.ts

@ -16,7 +16,7 @@ export default function getDownloadMediaDetails(options: DownloadMediaOptions) {
let downloadOptions: DownloadOptions; let downloadOptions: DownloadOptions;
if(media._ === 'document') downloadOptions = getDocumentDownloadOptions(media, thumb as any, queueId, onlyCache); 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); else if(isWebDocument(media)) downloadOptions = getWebDocumentDownloadOptions(media);
downloadOptions.downloadId = options.downloadId; downloadOptions.downloadId = options.downloadId;

5
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];
}

4
src/lib/mtproto/apiFileManager.ts

@ -13,7 +13,7 @@ import type {ReferenceBytes} from './referenceDatabase';
import Modes from '../../config/modes'; import Modes from '../../config/modes';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import {randomLong} from '../../helpers/random'; 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 {DcId} from '../../types';
import CacheStorageController from '../files/cacheStorage'; import CacheStorageController from '../files/cacheStorage';
import {logger, LogTypes} from '../logger'; import {logger, LogTypes} from '../logger';
@ -62,7 +62,7 @@ export type DownloadOptions = {
export type DownloadMediaOptions = { export type DownloadMediaOptions = {
media: Photo.photo | Document.document | WebDocument, media: Photo.photo | Document.document | WebDocument,
thumb?: PhotoSize, thumb?: PhotoSize | VideoSize,
queueId?: number, queueId?: number,
onlyCache?: boolean, onlyCache?: boolean,
downloadId?: string downloadId?: string

4
src/scss/components/_global.scss

@ -177,6 +177,10 @@ Utility Classes
pointer-events: none !important; pointer-events: none !important;
} }
.reflect-x {
transform: scaleX(-1);
}
/* .flex-grow { /* .flex-grow {
flex-grow: 1; flex-grow: 1;
} }

6
src/scss/partials/_emojiAnimation.scss

@ -10,12 +10,6 @@
// @include sidebar-transform(true); // @include sidebar-transform(true);
&.is-in {
.rlottie {
transform: scaleX(-1);
}
}
&-container { &-container {
position: absolute; position: absolute;
top: 0; top: 0;

18
src/scss/style.scss

@ -699,13 +699,27 @@ input:-webkit-autofill:active {
color: #fff; color: #fff;
font-size: 1rem; font-size: 1rem;
border-radius: $border-radius-medium; border-radius: $border-radius-medium;
animation: fade-in-opacity-fade-out-opacity 3s linear forwards;
z-index: 5; 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 { b {
color: inherit; color: inherit;
} }
a {
color: #60a5e9!important;
cursor: pointer;
}
} }
hr { hr {

Loading…
Cancel
Save