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';
export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' | export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' | '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 { export interface AnimationItem {
el: HTMLElement, el: HTMLElement,
group: AnimationItemGroup, group: AnimationItemGroup,
@ -201,7 +201,11 @@ export class AnimationIntersector {
} }
} }
public setOnlyOnePlayableGroup(group: AnimationItemGroup) { public getOnlyOnePlayableGroup() {
return this.onlyOnePlayableGroup;
}
public setOnlyOnePlayableGroup(group: AnimationItemGroup = '') {
this.onlyOnePlayableGroup = group; this.onlyOnePlayableGroup = group;
} }

8
src/components/chat/bubbles.ts

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

2
src/components/popups/stickers.ts

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

8
src/components/singleTransition.ts

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

60
src/components/wrappers/sticker.ts

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

48
src/components/wrappers/stickerAnimation.ts

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

2
src/global.d.ts vendored

@ -30,7 +30,7 @@ declare global {
type FiltersError = 'PINNED_DIALOGS_TOO_MUCH'; type FiltersError = 'PINNED_DIALOGS_TOO_MUCH';
type LocalFileError = ApiFileManagerError | ReferenceError | StorageError; 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' | type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' | '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';
import {Modify, SendMessageEmojiInteractionData} from '../../types'; import {Modify, SendMessageEmojiInteractionData} from '../../types';
import htmlToSpan from '../../helpers/dom/htmlToSpan'; import htmlToSpan from '../../helpers/dom/htmlToSpan';
import getVisibleRect from '../../helpers/dom/getVisibleRect'; 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 PopupCall from '../../components/call';
import copy from '../../helpers/object/copy'; import copy from '../../helpers/object/copy';
import getObjectKeysAndSort from '../../helpers/object/getObjectKeysAndSort'; import getObjectKeysAndSort from '../../helpers/object/getObjectKeysAndSort';
@ -92,6 +92,15 @@ import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount
import findUpClassName from '../../helpers/dom/findUpClassName'; import findUpClassName from '../../helpers/dom/findUpClassName';
import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent'; import {CLICK_EVENT_NAME} from '../../helpers/dom/clickEvent';
import PopupPayment from '../../components/popups/payment'; 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'; export const CHAT_ANIMATION_GROUP: AnimationItemGroup = 'chat';
@ -207,15 +216,17 @@ export class AppImManager extends EventListenerBase<{
this.setSettings(); this.setSettings();
rootScope.addEventListener('settings_updated', this.setSettings); rootScope.addEventListener('settings_updated', this.setSettings);
rootScope.addEventListener('premium_toggle', (isPremium) => { const onPremiumToggle = (isPremium: boolean) => {
document.body.classList.toggle('is-premium', isPremium); document.body.classList.toggle('is-premium', isPremium);
}); };
rootScope.addEventListener('premium_toggle', onPremiumToggle);
onPremiumToggle(rootScope.premium);
useHeavyAnimationCheck(() => { useHeavyAnimationCheck(() => {
animationIntersector.setOnlyOnePlayableGroup('lock'); animationIntersector.setOnlyOnePlayableGroup('lock');
animationIntersector.checkAnimations(true); animationIntersector.checkAnimations(true);
}, () => { }, () => {
animationIntersector.setOnlyOnePlayableGroup(''); animationIntersector.setOnlyOnePlayableGroup();
animationIntersector.checkAnimations(false); animationIntersector.checkAnimations(false);
}); });
@ -404,6 +415,251 @@ export class AppImManager extends EventListenerBase<{
}, useRafs); }, 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) => { apiManagerProxy.addEventListener('notificationBuild', (options) => {
if(this.chat.peerId === options.message.peerId && !idleController.isIdle) { if(this.chat.peerId === options.message.peerId && !idleController.isIdle) {
return; return;

133
src/scss/partials/_stickerViewer.scss

@ -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;
--btn-menu-transition: .2s cubic-bezier(.4, 0, .2, 1); --btn-menu-transition: .2s cubic-bezier(.4, 0, .2, 1);
--esg-transition: var(--btn-menu-transition); --esg-transition: var(--btn-menu-transition);
--input-transition: .2s ease-out; --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-function: cubic-bezier(.4, 0, .2, 1);
--popup-transition-time: .15s; --popup-transition-time: .15s;
//--layer-transition: .3s cubic-bezier(.33, 1, .68, 1); //--layer-transition: .3s cubic-bezier(.33, 1, .68, 1);
@ -357,6 +361,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/reactions"; @import "partials/reactions";
@import "partials/reaction"; @import "partials/reaction";
@import "partials/stackedAvatars"; @import "partials/stackedAvatars";
@import "partials/stickerViewer";
@import "partials/popups/popup"; @import "partials/popups/popup";
@import "partials/popups/editAvatar"; @import "partials/popups/editAvatar";

4
src/types.d.ts vendored

@ -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] [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 type AuthState = AuthState.signIn | AuthState.signQr | AuthState.authCode | AuthState.password | AuthState.signUp | AuthState.signedIn;
export namespace AuthState { export namespace AuthState {
export type signIn = { export type signIn = {

Loading…
Cancel
Save