diff --git a/.env b/.env index a752ffa6..0e2a81e3 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ API_ID=1025907 API_HASH=452b0359b988148995f22ff0f4229750 VERSION=1.0.4 -VERSION_FULL=1.0.4 (73) -BUILD=73 +VERSION_FULL=1.0.4 (74) +BUILD=74 diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 4da45e30..a508364d 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1338,15 +1338,39 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o let sendInteractionThrottled: () => void; - attachClickEvent(div, (e) => { - cancelEvent(e); - let animation = LottieLoader.getAnimation(div); + appStickersManager.preloadAnimatedEmojiStickerAnimation(emoji); + + attachClickEvent(div, async(e) => { + const animation = LottieLoader.getAnimation(div); if(animation.paused) { + const doc = appStickersManager.getAnimatedEmojiSoundDocument(emoji); + if(doc) { + const audio = document.createElement('audio'); + + try { + await appDocsManager.downloadDoc(doc); + + const cacheContext = appDownloadManager.getCacheContext(doc); + audio.src = cacheContext.url; + await onMediaLoad(audio); + + audio.addEventListener('ended', () => { + audio.src = ''; + }, {once: true}); + + audio.play(); + } catch(err) { + + } + } + animation.autoplay = true; animation.restart(); } + cancelEvent(e); + const doc = appStickersManager.getAnimatedEmojiSticker(emoji, true); if(!doc) { return; @@ -1375,7 +1399,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o animation.addEventListener('enterFrame', (frameNo) => { if(frameNo === animation.maxFrame) { animation.remove(); - // animationDiv.remove(); + animationDiv.remove(); appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll); } }); diff --git a/src/helpers/fixBase64String.ts b/src/helpers/fixBase64String.ts new file mode 100644 index 00000000..de1bebed --- /dev/null +++ b/src/helpers/fixBase64String.ts @@ -0,0 +1,7 @@ +export default function fixBase64String(str: string, toUrl: boolean) { + if(toUrl) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); + } else { + return str.replace(/-/g, '+').replace(/_/g, '/'); + } +} diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 773dc14e..6a208a25 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -19,6 +19,7 @@ import mediaSizes from '../../helpers/mediaSizes'; import { getEmojiToneIndex } from '../../vendor/emoji'; import RichTextProcessor from '../richtextprocessor'; import assumeType from '../../helpers/assumeType'; +import fixBase64String from '../../helpers/fixBase64String'; const CACHE_TIME = 3600e3; @@ -29,6 +30,8 @@ const LOCAL_IDS_SET = new Set([ EMOJI_ANIMATIONS_SET_LOCAL_ID ]); +// let TEST_FILE_REFERENCE_REFRESH = true; + export type MyStickerSetInput = { id: StickerSet.stickerSet['id'], access_hash?: StickerSet.stickerSet['access_hash'] @@ -39,14 +42,21 @@ export type MyMessagesStickerSet = MessagesStickerSet.messagesStickerSet; export class AppStickersManager { private storage = new AppStorage, typeof DATABASE_STATE>(DATABASE_STATE, 'stickerSets'); - private getStickerSetPromises: {[setId: Long]: Promise} = {}; - private getStickersByEmoticonsPromises: {[emoticon: string]: Promise} = {}; + private getStickerSetPromises: {[setId: Long]: Promise}; + private getStickersByEmoticonsPromises: {[emoticon: string]: Promise}; private greetingStickers: Document.document[]; private getGreetingStickersTimeout: number; private getGreetingStickersPromise: Promise; + + private sounds: Record; + getAnimatedEmojiSoundsPromise: Promise; constructor() { + this.getStickerSetPromises = {}; + this.getStickersByEmoticonsPromises = {}; + this.sounds = {}; + this.getAnimatedEmojiStickerSet(); rootScope.addMultipleEventsListeners({ @@ -143,12 +153,59 @@ export class AppStickersManager { public getAnimatedEmojiStickerSet() { return Promise.all([ this.getStickerSet({id: EMOJI_SET_LOCAL_ID}, {saveById: true}), - this.getStickerSet({id: EMOJI_ANIMATIONS_SET_LOCAL_ID}, {saveById: true}) + this.getStickerSet({id: EMOJI_ANIMATIONS_SET_LOCAL_ID}, {saveById: true}), + this.getAnimatedEmojiSounds() ]).then(([emoji, animations]) => { return {emoji, animations}; }); } + public getAnimatedEmojiSounds(overwrite?: boolean) { + if(this.getAnimatedEmojiSoundsPromise && !overwrite) return this.getAnimatedEmojiSoundsPromise; + return this.getAnimatedEmojiSoundsPromise = apiManager.getAppConfig(overwrite).then(appConfig => { + for(const emoji in appConfig.emojies_sounds) { + const sound = appConfig.emojies_sounds[emoji]; + const bytesStr = atob(fixBase64String(sound.file_reference_base64, false)); + const bytes = new Uint8Array(bytesStr.length); + for(let i = 0, length = bytes.length; i < length; ++i) { + bytes[i] = bytesStr[i].charCodeAt(0); + } + + // if(TEST_FILE_REFERENCE_REFRESH) { + // bytes[0] = bytes[1] = bytes[2] = bytes[3] = bytes[4] = 0; + // sound.access_hash += '999'; + // } + + const doc = appDocsManager.saveDoc({ + _: 'document', + pFlags: {}, + flags: 0, + id: sound.id, + access_hash: sound.access_hash, + attributes: [/* { + _: 'documentAttributeAudio', + duration: 1, + pFlags: {} + } */], + date: 0, + dc_id: rootScope.config.this_dc, + file_reference: bytes, + mime_type: 'audio/mp3', + size: 1 + // size: 101010 // test loading everytime + }, { + type: 'emojiesSounds' + }); + + this.sounds[emoji] = doc; + } + + // if(TEST_FILE_REFERENCE_REFRESH) { + // TEST_FILE_REFERENCE_REFRESH = false; + // } + }); + } + public async getRecentStickers(): Promise> { @@ -164,17 +221,25 @@ export class AppStickersManager { return res; } + private cleanEmoji(emoji: string) { + return emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, ''); + } + public getAnimatedEmojiSticker(emoji: string, isAnimation?: boolean) { const stickerSet = this.storage.getFromCache(isAnimation ? EMOJI_ANIMATIONS_SET_LOCAL_ID : EMOJI_SET_LOCAL_ID); if(!stickerSet || !stickerSet.documents) return undefined; - emoji = emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, ''); + emoji = this.cleanEmoji(emoji); const pack = stickerSet.packs.find(p => p.emoticon === emoji); return pack ? appDocsManager.getDoc(pack.documents[0]) : undefined; } + public getAnimatedEmojiSoundDocument(emoji: string) { + return this.sounds[this.cleanEmoji(emoji)]; + } + public preloadAnimatedEmojiSticker(emoji: string, width?: number, height?: number) { - return this.getAnimatedEmojiStickerSet().then(() => { + const preloadEmojiPromise = this.getAnimatedEmojiStickerSet().then(() => { const doc = this.getAnimatedEmojiSticker(emoji); if(doc) { return appDocsManager.downloadDoc(doc) @@ -199,6 +264,24 @@ export class AppStickersManager { }); } }); + + return Promise.all([ + preloadEmojiPromise, + this.preloadAnimatedEmojiStickerAnimation(emoji) + ]); + } + + public preloadAnimatedEmojiStickerAnimation(emoji: string) { + return this.getAnimatedEmojiStickerSet().then(() => { + const doc = this.getAnimatedEmojiSticker(emoji, true); + if(doc) { + const soundDoc = this.getAnimatedEmojiSoundDocument(emoji); + return Promise.all([ + appDocsManager.downloadDoc(doc), + soundDoc ? appDocsManager.downloadDoc(soundDoc) : undefined + ]); + } + }); } public saveStickerSet(res: Omit, id: DocId) { diff --git a/src/lib/mtproto/appConfig.d.ts b/src/lib/mtproto/appConfig.d.ts new file mode 100644 index 00000000..3422f4e4 --- /dev/null +++ b/src/lib/mtproto/appConfig.d.ts @@ -0,0 +1,51 @@ +export interface MTAppConfig { + test?: number; + emojies_animated_zoom?: number; + emojies_send_dice?: string[]; + emojies_send_dice_success?: EmojiesSendDiceSuccess; + emojies_sounds?: EmojiesSounds; + gif_search_branding?: string; + gif_search_emojies?: string[]; + stickers_emoji_suggest_only_api?: boolean; + stickers_emoji_cache_time?: number; + groupcall_video_participants_max?: number; + youtube_pip?: string; + qr_login_camera?: boolean; + qr_login_code?: string; + dialog_filters_enabled?: boolean; + dialog_filters_tooltip?: boolean; + ignore_restriction_reasons?: string[]; + autoarchive_setting_available?: boolean; + pending_suggestions?: any[]; + autologin_token?: string; + autologin_domains?: string[]; + round_video_encoding?: RoundVideoEncoding; + chat_read_mark_expire_period?: number; + chat_read_mark_size_threshold?: number; + reactions_default?: string; + reactions_uniq_max?: number; +} + +export interface EmojiesSendDiceSuccess { + [k]: EmojiesSendDiceSuccessDetails +} + +export interface EmojiesSendDiceSuccessDetails { + value?: number; + frame_start?: number; +} + +export type EmojiesSounds = Record; + +export interface EmojiSound { + id?: string; + access_hash?: string; + file_reference_base64?: string; +} + +export interface RoundVideoEncoding { + diameter?: number; + video_bitrate?: number; + audio_bitrate?: number; + max_size?: number; +} diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index f7e9e984..f49ae03a 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -7,7 +7,7 @@ import type { LocalStorageProxyTask, LocalStorageProxyTaskResponse } from '../localStorage'; //import type { LocalStorageProxyDeleteTask, LocalStorageProxySetTask } from '../storage'; import type { Awaited, InvokeApiOptions, WorkerTaskVoidTemplate } from '../../types'; -import type { Config, InputFile, MethodDeclMap, User } from '../../layer'; +import type { Config, InputFile, JSONValue, MethodDeclMap, User } from '../../layer'; import MTProtoWorker from 'worker-loader!./mtproto.worker'; //import './mtproto.worker'; import { isObject } from '../../helpers/object'; @@ -32,6 +32,7 @@ import { CacheStorageDbName } from '../cacheStorage'; import { pause } from '../../helpers/schedulers/pause'; import IS_WEBP_SUPPORTED from '../../environment/webpSupport'; import type { ApiError } from './apiManager'; +import { MTAppConfig } from './appConfig'; type Task = { taskId: number, @@ -105,6 +106,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { private postMessagesWaiting: any[][] = []; private getConfigPromise: Promise; + private getAppConfigPromise: Promise; constructor() { super(); @@ -693,6 +695,14 @@ export class ApiManagerProxy extends CryptoWorkerMethods { return config; }); } + + public getAppConfig(overwrite?: boolean): Promise { + if(this.getAppConfigPromise && !overwrite) return this.getAppConfigPromise; + return this.getAppConfigPromise = this.invokeApi('help.getAppConfig').then(config => { + rootScope.appConfig = config; + return config; + }); + } } const apiManagerProxy = new ApiManagerProxy(); diff --git a/src/lib/mtproto/referenceDatabase.ts b/src/lib/mtproto/referenceDatabase.ts index a925ba4c..df3d7b99 100644 --- a/src/lib/mtproto/referenceDatabase.ts +++ b/src/lib/mtproto/referenceDatabase.ts @@ -6,6 +6,7 @@ import { RefreshReferenceTask, RefreshReferenceTaskResponse } from "./apiFileManager"; import appMessagesManager from "../appManagers/appMessagesManager"; +import appStickersManager from "../appManagers/appStickersManager"; import { Photo } from "../../layer"; import { bytesToHex } from "../../helpers/bytes"; import { deepEqual } from "../../helpers/object"; @@ -14,7 +15,7 @@ import apiManager from "./mtprotoworker"; import assumeType from "../../helpers/assumeType"; import { logger } from "../logger"; -export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage; +export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds; export namespace ReferenceContext { export type referenceContextProfilePhoto = { type: 'profilePhoto', @@ -26,6 +27,10 @@ export namespace ReferenceContext { peerId: PeerId, messageId: number }; + + export type referenceContextEmojiesSounds = { + type: 'emojiesSounds' + }; } export type ReferenceBytes = Photo.photo['file_reference']; @@ -38,6 +43,7 @@ class ReferenceDatabase { //private references: Map = new Map(); private links: {[hex: string]: ReferenceBytes} = {}; private log = logger('RD', undefined, true); + private refreshEmojiesSoundsPromise: Promise; constructor() { apiManager.addTaskListener('refreshReference', (task: RefreshReferenceTask) => { @@ -125,6 +131,13 @@ class ReferenceDatabase { // }); } + case 'emojiesSounds': { + promise = this.refreshEmojiesSoundsPromise || appStickersManager.getAnimatedEmojiSounds(true).then(() => { + this.refreshEmojiesSoundsPromise = undefined; + }); + break; + } + default: { this.log.warn('refreshReference: not implemented context', context); return Promise.reject(); diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 87b3a2ff..ce85955c 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import type { Message, StickerSet, Update, NotifyPeer, PeerNotifySettings, ConstructorDeclMap, Config, PollResults, Poll, WebPage, GroupCall, GroupCallParticipant, PhoneCall } from "../layer"; +import type { Message, StickerSet, Update, NotifyPeer, PeerNotifySettings, ConstructorDeclMap, Config, PollResults, Poll, WebPage, GroupCall, GroupCallParticipant, PhoneCall, MethodDeclMap } from "../layer"; import type { MyDocument } from "./appManagers/appDocsManager"; import type { AppMessagesManager, Dialog, MessagesStorage, MyMessage } from "./appManagers/appMessagesManager"; import type { MyDialogFilter } from "./storages/filters"; @@ -23,6 +23,7 @@ import type Chat from "../components/chat/chat"; import { NULL_PEER_ID, UserAuth } from "./mtproto/mtproto_config"; import EventListenerBase from "../helpers/eventListenerBase"; import { MOUNT_CLASS_TO } from "../config/debug"; +import { MTAppConfig } from "./mtproto/appConfig"; export type BroadcastEvents = { 'chat_full_update': ChatId, @@ -183,6 +184,7 @@ export class RootScope extends EventListenerBase<{ message_length_max: 4096, caption_length_max: 1024, }; + public appConfig: MTAppConfig; public themeColor: string; private _themeColorElem: Element; diff --git a/src/pages/pageSignQR.ts b/src/pages/pageSignQR.ts index 728a4231..60947d78 100644 --- a/src/pages/pageSignQR.ts +++ b/src/pages/pageSignQR.ts @@ -19,6 +19,7 @@ import rootScope from '../lib/rootScope'; import { putPreloader } from '../components/misc'; import getLanguageChangeButton from '../components/languageChangeButton'; import { pause } from '../helpers/schedulers/pause'; +import fixBase64String from '../helpers/fixBase64String'; const FETCH_INTERVAL = 3; @@ -105,7 +106,7 @@ let onFirstMount = async() => { prevToken = loginToken.token; let encoded = bytesToBase64(loginToken.token); - let url = "tg://login?token=" + encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, ""); + let url = "tg://login?token=" + fixBase64String(encoded, true); const style = window.getComputedStyle(document.documentElement); const surfaceColor = style.getPropertyValue('--surface-color').trim();