Browse Source

Sticker viewer

master
Eduard Kuzmenko 2 years ago
parent
commit
3266d1d4c6
  1. 8
      src/components/animationIntersector.ts
  2. 8
      src/components/chat/bubbles.ts
  3. 2
      src/components/popups/stickers.ts
  4. 8
      src/components/singleTransition.ts
  5. 60
      src/components/wrappers/sticker.ts
  6. 48
      src/components/wrappers/stickerAnimation.ts
  7. 2
      src/global.d.ts
  8. 264
      src/lib/appManagers/appImManager.ts
  9. 133
      src/scss/partials/_stickerViewer.scss
  10. 5
      src/scss/style.scss
  11. 4
      src/types.d.ts

8
src/components/animationIntersector.ts

@ -16,7 +16,7 @@ import appMediaPlaybackController from './appMediaPlaybackController'; @@ -16,7 +16,7 @@ import appMediaPlaybackController from './appMediaPlaybackController';
export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' |
`CHAT-MENU-REACTIONS-${number}` | 'INLINE-HELPER' | 'GENERAL-SETTINGS';
`CHAT-MENU-REACTIONS-${number}` | 'INLINE-HELPER' | 'GENERAL-SETTINGS' | 'STICKER-VIEWER';
export interface AnimationItem {
el: HTMLElement,
group: AnimationItemGroup,
@ -201,7 +201,11 @@ export class AnimationIntersector { @@ -201,7 +201,11 @@ export class AnimationIntersector {
}
}
public setOnlyOnePlayableGroup(group: AnimationItemGroup) {
public getOnlyOnePlayableGroup() {
return this.onlyOnePlayableGroup;
}
public setOnlyOnePlayableGroup(group: AnimationItemGroup = '') {
this.onlyOnePlayableGroup = group;
}

8
src/components/chat/bubbles.ts

@ -111,6 +111,7 @@ import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText'; @@ -111,6 +111,7 @@ import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText';
import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount';
import PopupPayment from '../popups/payment';
import isInDOM from '../../helpers/dom/isInDOM';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -1234,9 +1235,6 @@ export default class ChatBubbles { @@ -1234,9 +1235,6 @@ export default class ChatBubbles {
needFadeIn: false
}).then(({render}) => render).then((player) => {
assumeType<RLottiePlayer>(player);
if(!middleware()) {
return;
}
player.addEventListener('firstFrame', () => {
if(!middleware()) {
@ -1254,7 +1252,7 @@ export default class ChatBubbles { @@ -1254,7 +1252,7 @@ export default class ChatBubbles {
this.managers.appReactionsManager.sendReaction(message, availableReaction.reaction);
this.unhoverPrevious();
}, {listenerSetter: this.listenerSetter});
});
}, noop);
});
} else if(hoverReaction.dataset.loaded) {
this.setHoverVisible(hoverReaction, true);
@ -4035,7 +4033,7 @@ export default class ChatBubbles { @@ -4035,7 +4033,7 @@ export default class ChatBubbles {
noPremium: messageMedia?.pFlags?.nopremium
});
if(isInUnread || isOutgoing/* || true */) {
if(getStickerEffectThumb(doc) && (isInUnread || isOutgoing)/* || true */) {
this.observer.observe(bubble, this.stickerEffectObserverCallback);
}
} else if(doc.type === 'video' || doc.type === 'gif' || doc.type === 'round'/* && doc.size <= 20e6 */) {

2
src/components/popups/stickers.ts

@ -33,7 +33,7 @@ export default class PopupStickers extends PopupElement { @@ -33,7 +33,7 @@ export default class PopupStickers extends PopupElement {
this.title.append(i18n('Loading'));
this.addEventListener('close', () => {
animationIntersector.setOnlyOnePlayableGroup('');
animationIntersector.setOnlyOnePlayableGroup();
});
const div = document.createElement('div');

8
src/components/singleTransition.ts

@ -12,7 +12,8 @@ const SetTransition = ( @@ -12,7 +12,8 @@ const SetTransition = (
forwards: boolean,
duration: number,
onTransitionEnd?: () => void,
useRafs?: number
useRafs?: number,
onTransitionStart?: () => void
) => {
const {timeout, raf} = element.dataset;
if(timeout !== undefined) {
@ -36,7 +37,7 @@ const SetTransition = ( @@ -36,7 +37,7 @@ const SetTransition = (
if(useRafs && rootScope.settings.animationsEnabled && duration) {
element.dataset.raf = '' + window.requestAnimationFrame(() => {
delete element.dataset.raf;
SetTransition(element, className, forwards, duration, onTransitionEnd, useRafs - 1);
SetTransition(element, className, forwards, duration, onTransitionEnd, useRafs - 1, onTransitionStart);
});
return;
@ -54,9 +55,10 @@ const SetTransition = ( @@ -54,9 +55,10 @@ const SetTransition = (
element.classList.remove('animating');
onTransitionEnd && onTransitionEnd();
onTransitionEnd?.();
};
onTransitionStart?.();
if(!rootScope.settings.animationsEnabled || !duration) {
element.classList.remove('animating', 'backwards');
afterTimeout();

60
src/components/wrappers/sticker.ts

@ -16,6 +16,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName'; @@ -16,6 +16,7 @@ import findUpClassName from '../../helpers/dom/findUpClassName';
import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl';
import getImageFromStrippedThumb from '../../helpers/getImageFromStrippedThumb';
import getPreviewURLFromThumb from '../../helpers/getPreviewURLFromThumb';
import makeError from '../../helpers/makeError';
import onMediaLoad from '../../helpers/onMediaLoad';
import {isSavingLottiePreview, saveLottiePreview} from '../../helpers/saveLottiePreview';
import throttle from '../../helpers/schedulers/throttle';
@ -33,7 +34,7 @@ import RLottiePlayer from '../../lib/rlottie/rlottiePlayer'; @@ -33,7 +34,7 @@ import RLottiePlayer from '../../lib/rlottie/rlottiePlayer';
import rootScope from '../../lib/rootScope';
import type {ThumbCache} from '../../lib/storages/thumbs';
import webpWorkerController from '../../lib/webp/webpWorkerController';
import {SendMessageEmojiInteractionData} from '../../types';
import {Awaited, SendMessageEmojiInteractionData} from '../../types';
import {getEmojiToneIndex} from '../../vendor/emoji';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import LazyLoadQueue from '../lazyLoadQueue';
@ -42,12 +43,12 @@ import {hideToast, toastNew} from '../toast'; @@ -42,12 +43,12 @@ import {hideToast, toastNew} from '../toast';
import wrapStickerAnimation from './stickerAnimation';
// 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;
export const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2;
const EMOJI_EFFECT_MULTIPLIER = 3;
const locksUrls: {[docId: string]: string} = {};
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, noPremium, withLock}: {
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, noPremium, withLock, relativeEffect, loopEffect}: {
doc: MyDocument,
div: HTMLElement,
middleware?: () => boolean,
@ -69,7 +70,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -69,7 +70,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
fullThumb?: PhotoSize | VideoSize,
isOut?: boolean,
noPremium?: boolean,
withLock?: boolean
withLock?: boolean,
relativeEffect?: boolean,
loopEffect?: boolean
}) {
const stickerType = doc.sticker;
if(stickerType === 1) {
@ -305,8 +308,11 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -305,8 +308,11 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
return;
}
const middlewareError = makeError('MIDDLEWARE');
const load = async() => {
if(middleware && !middleware()) return;
if(middleware && !middleware()) {
throw middlewareError;
}
if(stickerType === 2 && !asStatic) {
/* if(doc.id === '1860749763008266301') {
@ -325,7 +331,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -325,7 +331,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
// console.timeEnd('download sticker' + doc.id);
// console.log('loaded sticker:', doc, div/* , blob */);
if(middleware && !middleware()) {
throw new Error('wrapSticker 2 middleware');
throw middlewareError;
}
const animation = await lottieLoader.loadAnimationWorker({
@ -355,7 +361,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -355,7 +361,7 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
}
const cb = () => {
if(element && element !== animation.canvas) {
if(element && element !== animation.canvas && element.tagName !== 'DIV') {
element.remove();
}
};
@ -513,6 +519,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -513,6 +519,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
if(play) {
(media as HTMLVideoElement).autoplay = true;
}
if(loop) {
(media as HTMLVideoElement).loop = true;
}
}
@ -528,16 +537,17 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -528,16 +537,17 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
media.classList.add('fade-in');
}
return new Promise<void>(async(resolve, reject) => {
return new Promise<HTMLVideoElement | HTMLImageElement>(async(resolve, reject) => {
const r = async() => {
if(middleware && !middleware()) return resolve();
if(middleware && !middleware()) {
reject(middlewareError);
return;
}
const onLoad = () => {
sequentialDom.mutateElement(div, () => {
div.append(media);
if(thumbImage) {
thumbImage.classList.add('fade-out');
}
thumbImage && thumbImage.classList.add('fade-out');
if(stickerType === 3 && !isSavingLottiePreview(doc, toneIndex)) {
// const perf = performance.now();
@ -555,14 +565,12 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -555,14 +565,12 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
animationIntersector.addAnimation(media as HTMLVideoElement, group);
}
resolve();
resolve(media as any);
if(needFadeIn) {
media.addEventListener('animationend', () => {
media.classList.remove('fade-in');
if(thumbImage) {
thumbImage.remove();
}
thumbImage?.remove();
}, {once: true});
}
});
@ -589,13 +597,13 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -589,13 +597,13 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
promise = appDownloadManager.downloadMediaURL({media: doc, queueId: lazyLoadQueue?.queueId});
}
promise.then(r, resolve);
promise.then(r, reject);
}
});
}
};
const loadPromise: Promise<RLottiePlayer | void> = lazyLoadQueue && (!downloaded || isAnimated) ?
const loadPromise: Promise<Awaited<ReturnType<typeof load>> | void> = lazyLoadQueue && (!downloaded || isAnimated) ?
(lazyLoadQueue.push({div, load}), Promise.resolve()) :
load();
@ -614,21 +622,25 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue, @@ -614,21 +622,25 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
middleware,
isOut,
width,
loadPromise
loadPromise,
relativeEffect,
loopEffect
});
}
return {render: loadPromise};
}
function attachStickerEffectHandler({container, doc, managers, middleware, isOut, width, loadPromise}: {
function attachStickerEffectHandler({container, doc, managers, middleware, isOut, width, loadPromise, relativeEffect, loopEffect}: {
container: HTMLElement,
doc: MyDocument,
managers: AppManagers,
middleware: () => boolean,
isOut: boolean,
width: number,
loadPromise: Promise<any>
loadPromise: Promise<any>,
relativeEffect?: boolean,
loopEffect?: boolean
}) {
managers.appStickersManager.preloadSticker(doc.id, true);
@ -660,10 +672,12 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut @@ -660,10 +672,12 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut
size: width * STICKER_EFFECT_MULTIPLIER,
target: container,
play: true,
fullThumb: getStickerEffectThumb(doc)
fullThumb: getStickerEffectThumb(doc),
relativeEffect,
loopEffect
});
if(isOut !== undefined && !isOut) {
if(isOut !== undefined && !isOut/* && !relativeEffect */) {
animationDiv.classList.add('reflect-x');
}

48
src/components/wrappers/stickerAnimation.ts

@ -26,7 +26,9 @@ export default function wrapStickerAnimation({ @@ -26,7 +26,9 @@ export default function wrapStickerAnimation({
play,
managers,
fullThumb,
withRandomOffset
withRandomOffset,
relativeEffect,
loopEffect
}: {
size: number,
doc: MyDocument,
@ -37,7 +39,9 @@ export default function wrapStickerAnimation({ @@ -37,7 +39,9 @@ export default function wrapStickerAnimation({
play: boolean,
managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize,
withRandomOffset?: boolean
withRandomOffset?: boolean,
relativeEffect?: boolean,
loopEffect?: boolean
}) {
const animationDiv = document.createElement('div');
animationDiv.classList.add('emoji-animation');
@ -59,7 +63,7 @@ export default function wrapStickerAnimation({ @@ -59,7 +63,7 @@ export default function wrapStickerAnimation({
middleware,
withThumb: false,
needFadeIn: false,
loop: false,
loop: !!loopEffect,
width: size,
height: size,
play,
@ -71,7 +75,7 @@ export default function wrapStickerAnimation({ @@ -71,7 +75,7 @@ export default function wrapStickerAnimation({
assumeType<RLottiePlayer>(_animation);
animation = _animation;
animation.addEventListener('enterFrame', (frameNo) => {
if(frameNo === animation.maxFrame || !isInDOM(target)) {
if((!loopEffect && frameNo === animation.maxFrame) || !isInDOM(target)) {
unmountAnimation();
}
});
@ -92,7 +96,6 @@ export default function wrapStickerAnimation({ @@ -92,7 +96,6 @@ export default function wrapStickerAnimation({
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)) {
unmountAnimation();
@ -100,35 +103,46 @@ export default function wrapStickerAnimation({ @@ -100,35 +103,46 @@ export default function wrapStickerAnimation({
}
const rect = target.getBoundingClientRect();
/* const boxWidth = Math.max(rect.width, rect.height);
const boxHeight = Math.max(rect.width, rect.height);
const x = rect.left + ((boxWidth - size) / 2);
const y = rect.top + ((boxHeight - size) / 2); */
const factor = rect.width / 200;
const stableOffsetX = side === 'center' ? 0 : 16 * (side === 'right' ? 1 : -1) * factor;
// const stableOffsetY = side === 'center' ? 0 : -50 * factor;
const stableOffsetY = side === 'center' ? 0 : 0 * factor;
const rectX = side === 'right' ? rect.right : rect.left;
const rectY = rect.top;
const addOffsetX = side === 'center' ? (rect.width - size) / 2 : (side === 'right' ? -size : 0) + stableOffsetX + randomOffsetX;
const addOffsetX = (side === 'center' ? (rect.width - size) / 2 : (side === 'right' ? -size : 0)) + stableOffsetX + randomOffsetX;
const addOffsetY = (side === 'center' || true ? (rect.height - size) / 2 : 0) + stableOffsetY + randomOffsetY;
const x = rectX + addOffsetX;
// const y = rect.bottom - size + size / 4;
const y = rect.top + ((rect.height - size) / 2) + (side === 'center' ? 0 : randomOffsetY);
// animationDiv.style.transform = `translate(${x}px, ${y}px)`;
const y = rectY + addOffsetY;
if(y <= -size || y >= windowSize.height) {
unmountAnimation();
return;
}
animationDiv.style.top = y + 'px';
animationDiv.style.left = x + 'px';
if(relativeEffect) {
if(side !== 'center') animationDiv.style[side] = Math.abs(stableOffsetX) * -1 + 'px';
else animationDiv.style.left = addOffsetX + 'px';
animationDiv.style.top = addOffsetY + 'px';
} else {
animationDiv.style.top = y + 'px';
animationDiv.style.left = x + 'px';
}
};
const onScroll = throttleWithRaf(setPosition);
appImManager.chat.bubbles.scrollable.container.addEventListener('scroll', onScroll);
setPosition();
appImManager.emojiAnimationContainer.append(animationDiv);
if(relativeEffect) {
animationDiv.classList.add('is-relative');
target.parentElement.append(animationDiv);
} else {
appImManager.emojiAnimationContainer.append(animationDiv);
}
return {animationDiv, stickerPromise};
}

2
src/global.d.ts vendored

@ -30,7 +30,7 @@ declare global { @@ -30,7 +30,7 @@ declare global {
type FiltersError = 'PINNED_DIALOGS_TOO_MUCH';
type LocalFileError = ApiFileManagerError | ReferenceError | StorageError;
type LocalErrorType = LocalFileError | NetworkerError | FiltersError | 'UNKNOWN' | 'NO_DOC';
type LocalErrorType = LocalFileError | NetworkerError | FiltersError | 'UNKNOWN' | 'NO_DOC' | 'MIDDLEWARE';
type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' |

264
src/lib/appManagers/appImManager.ts

@ -55,7 +55,7 @@ import {CallType} from '../calls/types'; @@ -55,7 +55,7 @@ import {CallType} from '../calls/types';
import {Modify, SendMessageEmojiInteractionData} from '../../types';
import htmlToSpan from '../../helpers/dom/htmlToSpan';
import getVisibleRect from '../../helpers/dom/getVisibleRect';
import {simulateClickEvent} from '../../helpers/dom/clickEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
import PopupCall from '../../components/call';
import copy from '../../helpers/object/copy';
import getObjectKeysAndSort from '../../helpers/object/getObjectKeysAndSort';
@ -92,6 +92,15 @@ import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount @@ -92,6 +92,15 @@ import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount
import findUpClassName from '../../helpers/dom/findUpClassName';
import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent';
import PopupPayment from '../../components/popups/payment';
import {getMiddleware} from '../../helpers/middleware';
import {wrapSticker} from '../../components/wrappers';
import windowSize from '../../helpers/windowSize';
import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb';
import {makeMediaSize} from '../../helpers/mediaSize';
import RLottiePlayer from '../rlottie/rlottiePlayer';
import type {MyDocument} from './appDocsManager';
import deferredPromise from '../../helpers/cancellablePromise';
import {STICKER_EFFECT_MULTIPLIER} from '../../components/wrappers/sticker';
export const CHAT_ANIMATION_GROUP: AnimationItemGroup = 'chat';
@ -207,15 +216,17 @@ export class AppImManager extends EventListenerBase<{ @@ -207,15 +216,17 @@ export class AppImManager extends EventListenerBase<{
this.setSettings();
rootScope.addEventListener('settings_updated', this.setSettings);
rootScope.addEventListener('premium_toggle', (isPremium) => {
const onPremiumToggle = (isPremium: boolean) => {
document.body.classList.toggle('is-premium', isPremium);
});
};
rootScope.addEventListener('premium_toggle', onPremiumToggle);
onPremiumToggle(rootScope.premium);
useHeavyAnimationCheck(() => {
animationIntersector.setOnlyOnePlayableGroup('lock');
animationIntersector.checkAnimations(true);
}, () => {
animationIntersector.setOnlyOnePlayableGroup('');
animationIntersector.setOnlyOnePlayableGroup();
animationIntersector.checkAnimations(false);
});
@ -404,6 +415,251 @@ export class AppImManager extends EventListenerBase<{ @@ -404,6 +415,251 @@ export class AppImManager extends EventListenerBase<{
}, useRafs);
};
let hasViewer = false;
!IS_TOUCH_SUPPORTED && document.addEventListener('mousedown', (e) => {
if(hasViewer || e.buttons > 1 || e.button !== 0) return;
let mediaContainer = findUpClassName(e.target, 'media-sticker-wrapper');
if(!mediaContainer) {
return;
}
// const img: HTMLImageElement = mediaContainer.querySelector('img.media-sticker');
const docId = mediaContainer.dataset.docId;
if(!docId) {
return;
}
const className = 'sticker-viewer';
const group: AnimationItemGroup = 'STICKER-VIEWER';
const openDuration = 200;
const switchDuration = 200;
const previousGroup = animationIntersector.getOnlyOnePlayableGroup();
const _middleware = getMiddleware();
let container: HTMLElement, previousTransformer: HTMLElement;
const doThatSticker = async({mediaContainer, doc, middleware, lockGroups, isSwitching}: {
mediaContainer: HTMLElement,
doc: MyDocument,
middleware: () => boolean,
lockGroups?: boolean,
isSwitching?: boolean
}) => {
const effectThumb = getStickerEffectThumb(doc);
const mediaRect: DOMRect = mediaContainer.getBoundingClientRect();
const s = makeMediaSize(doc.w, doc.h);
const size = effectThumb ? 280 : 360;
const boxSize = makeMediaSize(size, size);
const fitted = mediaRect.width === mediaRect.height ? boxSize : s.aspectFitted(boxSize);
const bubble = findUpClassName(mediaContainer, 'bubble');
const isOut = bubble ? bubble.classList.contains('is-out') : true;
const transformer = document.createElement('div');
transformer.classList.add(className + '-transformer');
const stickerContainer = document.createElement('div');
stickerContainer.classList.add(className + '-sticker');
/* transformer.style.width = */stickerContainer.style.width = fitted.width + 'px';
/* transformer.style.height = */stickerContainer.style.height = fitted.height + 'px';
const stickerEmoji = document.createElement('div');
stickerEmoji.classList.add(className + '-emoji');
stickerEmoji.append(wrapEmojiText(doc.stickerEmojiRaw));
if(effectThumb) {
const margin = (size * STICKER_EFFECT_MULTIPLIER - size) / 3 * (isOut ? 1 : -1);
transformer.classList.add('has-effect');
// const property = `--margin-${isOut ? 'right' : 'left'}`;
// stickerContainer.style.setProperty(property, `${margin * 2}px`);
transformer.style.setProperty('--translateX', `${margin}px`);
stickerEmoji.style.setProperty('--translateX', `${-margin}px`);
}
const overflowElement = findUpClassName(mediaContainer, 'scrollable');
const visibleRect = getVisibleRect(mediaContainer, overflowElement, true, mediaRect);
if(visibleRect.overflow.vertical || visibleRect.overflow.horizontal) {
stickerContainer.classList.add('is-overflow');
}
// if(img) {
// const ratio = img.naturalWidth / img.naturalHeight;
// if((mediaRect.width / mediaRect.height).toFixed(1) !== ratio.toFixed(1)) {
// mediaRect = mediaRect.toJSON();
// }
// }
const rect = mediaContainer.getBoundingClientRect();
const scaleX = rect.width / fitted.width;
const scaleY = rect.height / fitted.height;
const transformX = rect.left - (windowSize.width - rect.width) / 2;
const transformY = rect.top - (windowSize.height - rect.height) / 2;
transformer.style.transform = `translate(${transformX}px, ${transformY}px) scale(${scaleX}, ${scaleY})`;
if(isSwitching) transformer.classList.add('is-switching');
transformer.append(stickerContainer, stickerEmoji);
container.append(transformer);
const player = await wrapSticker({
doc,
div: stickerContainer,
group,
width: fitted.width,
height: fitted.height,
play: false,
loop: true,
middleware,
managers: this.managers,
needFadeIn: false,
isOut,
withThumb: false,
relativeEffect: true,
loopEffect: true
}).then(({render}) => render);
if(!middleware()) return;
if(!container.parentElement) {
document.body.append(container);
}
const firstFramePromise = player instanceof RLottiePlayer ?
new Promise<void>((resolve) => player.addEventListener('firstFrame', resolve, {once: true})) :
Promise.resolve();
await Promise.all([firstFramePromise, doubleRaf()]);
await pause(0); // ! need it because firstFrame will be called just from the loop
if(!middleware()) return;
if(lockGroups) {
animationIntersector.setOnlyOnePlayableGroup(group);
animationIntersector.checkAnimations(true);
}
if(player instanceof RLottiePlayer) {
const prevPlayer = lottieLoader.getAnimation(mediaContainer);
player.curFrame = prevPlayer.curFrame;
player.play();
await new Promise<void>((resolve) => {
let i = 0;
const c = () => {
if(++i === 2) {
resolve();
player.removeEventListener('enterFrame', c);
}
};
player.addEventListener('enterFrame', c);
});
player.pause();
} else if(player instanceof HTMLVideoElement) {
player.currentTime = (mediaContainer.querySelector('video') as HTMLVideoElement).currentTime;
}
return {
ready: () => {
if(player instanceof RLottiePlayer || player instanceof HTMLVideoElement) {
player.play();
}
if(effectThumb) {
simulateClickEvent(stickerContainer);
}
},
transformer
};
};
const timeout = window.setTimeout(async() => {
document.removeEventListener('mousemove', onMousePreMove);
container = document.createElement('div');
container.classList.add(className);
hasViewer = true;
const middleware = _middleware.get();
const doc = await this.managers.appDocsManager.getDoc(docId);
if(!middleware()) return;
const {ready, transformer} = await doThatSticker({
doc,
mediaContainer,
middleware,
lockGroups: true
});
previousTransformer = transformer;
SetTransition(container, 'is-visible', true, openDuration, () => {
if(!middleware()) return;
ready();
});
document.addEventListener('mousemove', onMouseMove);
}, 100);
const onMouseMove = async(e: MouseEvent) => {
const newMediaContainer = findUpClassName(e.target, 'media-sticker-wrapper');
if(!newMediaContainer || mediaContainer === newMediaContainer) {
return;
}
const docId = newMediaContainer.dataset.docId;
if(!docId) {
return;
}
mediaContainer = newMediaContainer;
_middleware.clean();
const middleware = _middleware.get();
const doc = await this.managers.appDocsManager.getDoc(docId);
if(!middleware()) return;
const {ready, transformer} = await doThatSticker({
doc,
mediaContainer,
middleware,
isSwitching: true
});
const _previousTransformer = previousTransformer;
SetTransition(_previousTransformer, 'is-switching', true, switchDuration, () => {
_previousTransformer.remove();
});
previousTransformer = transformer;
SetTransition(transformer, 'is-switching', false, switchDuration, () => {
if(!middleware()) return;
ready();
});
};
const onMousePreMove = () => {
clearTimeout(timeout);
};
const onMouseUp = () => {
clearTimeout(timeout);
_middleware.clean();
if(container) {
SetTransition(container, 'is-visible', false, openDuration, () => {
container.remove();
animationIntersector.setOnlyOnePlayableGroup(previousGroup);
animationIntersector.checkAnimations(false);
hasViewer = false;
});
attachClickEvent(document.body, cancelEvent, {capture: true, once: true});
}
document.removeEventListener('mousemove', onMouseMove);
};
document.addEventListener('mousemove', onMousePreMove, {once: true});
document.addEventListener('mouseup', onMouseUp, {once: true});
});
apiManagerProxy.addEventListener('notificationBuild', (options) => {
if(this.chat.peerId === options.message.peerId && !idleController.isIdle) {
return;

133
src/scss/partials/_stickerViewer.scss

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.sticker-viewer {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 4;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
&:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, .6);
opacity: 0;
content: " ";
@include animation-level(2) {
transition: opacity var(--sticker-viewer-open-transition-out);
}
}
&.is-visible {
&:not(.backwards) {
&:before {
opacity: 1;
@include animation-level(2) {
transition: opacity var(--sticker-viewer-open-transition-in);
}
}
.sticker-viewer-transformer:not(.is-switching) {
transform: translateX(var(--translateX)) scale(1, 1) !important;
@include animation-level(2) {
transition: transform var(--sticker-viewer-open-transition-in);
}
}
.sticker-viewer-emoji,
.sticker-viewer-sticker,
.emoji-animation {
opacity: 1;
@include animation-level(2) {
transition: opacity var(--sticker-viewer-open-transition-in);
}
}
.sticker-viewer-sticker:not(.is-overflow) {
@include animation-level(2) {
transition: opacity 0s;
}
}
}
}
&-transformer {
--translateX: 0;
position: absolute;
// transform: translateX(0) scale(1);
display: flex;
justify-content: center;
align-items: center;
width: 360px;
height: 360px;
&.has-effect {
width: 280px;
height: 280px;
.sticker-viewer-emoji {
top: -5.5rem;
}
}
@include animation-level(2) {
transition: transform var(--sticker-viewer-open-transition-out);
}
&.is-switching {
transform: translateX(var(--translateX)) scale(1) !important;
opacity: 1 !important;
@include animation-level(2) {
transition: transform var(--sticker-viewer-switch-transition), opacity var(--sticker-viewer-switch-transition) !important;
}
&:not(.backwards) {
transform: scale(0.4) translateX(var(--translateX)) !important;
opacity: 0 !important;
}
}
}
&-emoji {
position: absolute;
top: -3rem;
transform: translateX(var(--translateX)) scale(2);
}
&-sticker {
position: absolute;
}
// &-sticker,
// .emoji-animation {
// margin-left: var(--margin-left);
// margin-right: var(--margin-right);
// }
&-emoji,
&-sticker,
.emoji-animation {
opacity: 0;
@include animation-level(2) {
transition: opacity var(--sticker-viewer-open-transition-out);
}
}
}

5
src/scss/style.scss

@ -68,6 +68,10 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -68,6 +68,10 @@ $chat-input-inner-padding-handhelds: .25rem;
--btn-menu-transition: .2s cubic-bezier(.4, 0, .2, 1);
--esg-transition: var(--btn-menu-transition);
--input-transition: .2s ease-out;
--sticker-viewer-open-transition-in: .2s var(--transition-standard-easing);
--sticker-viewer-open-transition-out: .2s var(--transition-standard-easing);
// --sticker-viewer-switch-transition: .2s cubic-bezier(.07,1.21,.56,1.2);
--sticker-viewer-switch-transition: .2s cubic-bezier(.12,1.1,.56,1.2);
--popup-transition-function: cubic-bezier(.4, 0, .2, 1);
--popup-transition-time: .15s;
//--layer-transition: .3s cubic-bezier(.33, 1, .68, 1);
@ -357,6 +361,7 @@ $chat-input-inner-padding-handhelds: .25rem; @@ -357,6 +361,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/reactions";
@import "partials/reaction";
@import "partials/stackedAvatars";
@import "partials/stickerViewer";
@import "partials/popups/popup";
@import "partials/popups/editAvatar";

4
src/types.d.ts vendored

@ -84,6 +84,10 @@ type ModifyFunctionsToAsync<T> = { @@ -84,6 +84,10 @@ type ModifyFunctionsToAsync<T> = {
[key in keyof T]: T[key] extends (...args: infer A) => infer R ? (R extends PromiseLike<infer O> ? T[key] : (...args: A) => Promise<Awaited<R>>) : T[key]
};
export type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
export type AuthState = AuthState.signIn | AuthState.signQr | AuthState.authCode | AuthState.password | AuthState.signUp | AuthState.signedIn;
export namespace AuthState {
export type signIn = {

Loading…
Cancel
Save