Browse Source

Premium stickers

Faster file downloading
master
Eduard Kuzmenko 2 years ago
parent
commit
b9e6151d5c
  1. 41
      src/components/animationIntersector.ts
  2. 14
      src/components/appSearch.ts
  3. 3
      src/components/appSearchSuper..ts
  4. 33
      src/components/chat/bubbles.ts
  5. 3
      src/components/chat/inlineHelper.ts
  6. 13
      src/components/chat/input.ts
  7. 6
      src/components/chat/reactionsMenu.ts
  8. 4
      src/components/emoticonsDropdown/index.ts
  9. 76
      src/components/emoticonsDropdown/tabs/stickers.ts
  10. 4
      src/components/gifsMasonry.ts
  11. 7
      src/components/popups/stickers.ts
  12. 5
      src/components/scrollable.ts
  13. 2
      src/components/sidebarLeft/index.ts
  14. 4
      src/components/sidebarRight/tabs/gifs.ts
  15. 3
      src/components/sidebarRight/tabs/stickers.ts
  16. 143
      src/components/wrappers/sticker.ts
  17. 26
      src/components/wrappers/stickerAnimation.ts
  18. 4
      src/components/wrappers/stickerSetThumb.ts
  19. 4
      src/components/wrappers/video.ts
  20. 3
      src/config/app.ts
  21. 28
      src/helpers/averageColor.ts
  22. 31
      src/helpers/computeLockColor.ts
  23. 2
      src/helpers/fileName.ts
  24. 3
      src/helpers/overlayClickHandler.ts
  25. 9
      src/helpers/setWorkerProxy.ts
  26. 1
      src/lang.ts
  27. 10
      src/lib/appManagers/appDialogsManager.ts
  28. 6
      src/lib/appManagers/appImManager.ts
  29. 28
      src/lib/appManagers/appManagersManager.ts
  30. 10
      src/lib/appManagers/appMessagesManager.ts
  31. 12
      src/lib/appManagers/appStickersManager.ts
  32. 9
      src/lib/crypto/crypto.worker.ts
  33. 21
      src/lib/crypto/cryptoMessagePort.ts
  34. 21
      src/lib/mtproto/apiFileManager.ts
  35. 1
      src/lib/mtproto/mtproto.worker.ts
  36. 1
      src/lib/mtproto/mtprotoMessagePort.ts
  37. 62
      src/lib/mtproto/mtprotoworker.ts
  38. 13
      src/lib/mtproto/networker.ts
  39. 6
      src/lib/mtproto/transports/http.ts
  40. 7
      src/lib/mtproto/transports/obfuscation.ts
  41. 6
      src/lib/mtproto/transports/tcpObfuscated.ts
  42. 8
      src/lib/rlottie/lottieLoader.ts
  43. 10
      src/lib/rlottie/rlottiePlayer.ts
  44. 8
      src/scss/mixins/_premium.scss
  45. 40
      src/scss/style.scss

41
src/components/animationIntersector.ts

@ -14,9 +14,12 @@ import forEachReverse from '../helpers/array/forEachReverse';
import idleController from '../helpers/idleController'; import idleController from '../helpers/idleController';
import appMediaPlaybackController from './appMediaPlaybackController'; 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';
export interface AnimationItem { export interface AnimationItem {
el: HTMLElement, el: HTMLElement,
group: string, group: AnimationItemGroup,
animation: RLottiePlayer | HTMLVideoElement animation: RLottiePlayer | HTMLVideoElement
}; };
@ -25,11 +28,11 @@ export class AnimationIntersector {
private visible: Set<AnimationItem> = new Set(); private visible: Set<AnimationItem> = new Set();
private overrideIdleGroups: Set<string>; private overrideIdleGroups: Set<string>;
private byGroups: {[group: string]: AnimationItem[]} = {}; private byGroups: {[group in AnimationItemGroup]?: AnimationItem[]} = {};
private lockedGroups: {[group: string]: true} = {}; private lockedGroups: {[group in AnimationItemGroup]?: true} = {};
private onlyOnePlayableGroup: string = ''; private onlyOnePlayableGroup: AnimationItemGroup = '';
private intersectionLockedGroups: {[group: string]: true} = {}; private intersectionLockedGroups: {[group in AnimationItemGroup]?: true} = {};
private videosLocked = false; private videosLocked = false;
constructor() { constructor() {
@ -40,11 +43,11 @@ export class AnimationIntersector {
const target = entry.target; const target = entry.target;
for(const group in this.byGroups) { for(const group in this.byGroups) {
if(this.intersectionLockedGroups[group]) { if(this.intersectionLockedGroups[group as AnimationItemGroup]) {
continue; continue;
} }
const player = this.byGroups[group].find((p) => p.el === target); const player = this.byGroups[group as AnimationItemGroup].find((p) => p.el === target);
if(player) { if(player) {
if(entry.isIntersecting) { if(entry.isIntersecting) {
this.visible.add(player); this.visible.add(player);
@ -104,7 +107,7 @@ export class AnimationIntersector {
public getAnimations(element: HTMLElement) { public getAnimations(element: HTMLElement) {
const found: AnimationItem[] = []; const found: AnimationItem[] = [];
for(const group in this.byGroups) { for(const group in this.byGroups) {
for(const player of this.byGroups[group]) { for(const player of this.byGroups[group as AnimationItemGroup]) {
if(player.el === element) { if(player.el === element) {
found.push(player); found.push(player);
} }
@ -138,8 +141,8 @@ export class AnimationIntersector {
this.visible.delete(player); this.visible.delete(player);
} }
public addAnimation(animation: RLottiePlayer | HTMLVideoElement, group = '') { public addAnimation(animation: RLottiePlayer | HTMLVideoElement, group: AnimationItemGroup = '') {
const player = { const player: AnimationItem = {
el: animation instanceof RLottiePlayer ? animation.el : animation, el: animation instanceof RLottiePlayer ? animation.el : animation,
animation: animation, animation: animation,
group group
@ -151,11 +154,11 @@ export class AnimationIntersector {
} }
} }
(this.byGroups[group] ?? (this.byGroups[group] = [])).push(player); (this.byGroups[group as AnimationItemGroup] ??= []).push(player);
this.observer.observe(player.el); this.observer.observe(player.el);
} }
public checkAnimations(blurred?: boolean, group?: string, destroy = false) { public checkAnimations(blurred?: boolean, group?: AnimationItemGroup, destroy = false) {
// if(rootScope.idle.isIDLE) return; // if(rootScope.idle.isIDLE) return;
if(group !== undefined && !this.byGroups[group]) { if(group !== undefined && !this.byGroups[group]) {
@ -163,7 +166,7 @@ export class AnimationIntersector {
return; return;
} }
const groups = group !== undefined /* && false */ ? [group] : Object.keys(this.byGroups); const groups = group !== undefined /* && false */ ? [group] : Object.keys(this.byGroups) as AnimationItemGroup[];
for(const group of groups) { for(const group of groups) {
const animations = this.byGroups[group]; const animations = this.byGroups[group];
@ -198,20 +201,20 @@ export class AnimationIntersector {
} }
} }
public setOnlyOnePlayableGroup(group: string) { public setOnlyOnePlayableGroup(group: AnimationItemGroup) {
this.onlyOnePlayableGroup = group; this.onlyOnePlayableGroup = group;
} }
public lockGroup(group: string) { public lockGroup(group: AnimationItemGroup) {
this.lockedGroups[group] = true; this.lockedGroups[group] = true;
} }
public unlockGroup(group: string) { public unlockGroup(group: AnimationItemGroup) {
delete this.lockedGroups[group]; delete this.lockedGroups[group];
this.checkAnimations(undefined, group); this.checkAnimations(undefined, group);
} }
public refreshGroup(group: string) { public refreshGroup(group: AnimationItemGroup) {
const animations = this.byGroups[group]; const animations = this.byGroups[group];
if(animations && animations.length) { if(animations && animations.length) {
animations.forEach((animation) => { animations.forEach((animation) => {
@ -226,11 +229,11 @@ export class AnimationIntersector {
} }
} }
public lockIntersectionGroup(group: string) { public lockIntersectionGroup(group: AnimationItemGroup) {
this.intersectionLockedGroups[group] = true; this.intersectionLockedGroups[group] = true;
} }
public unlockIntersectionGroup(group: string) { public unlockIntersectionGroup(group: AnimationItemGroup) {
delete this.intersectionLockedGroups[group]; delete this.intersectionLockedGroups[group];
this.refreshGroup(group); this.refreshGroup(group);
} }

14
src/components/appSearch.ts

@ -23,7 +23,8 @@ export class SearchGroup {
className?: string, className?: string,
clickable = true, clickable = true,
public autonomous = true, public autonomous = true,
public onFound?: () => void public onFound?: () => void,
public noIcons?: boolean
) { ) {
this.list = appDialogsManager.createChatList(); this.list = appDialogsManager.createChatList();
this.container = document.createElement('div'); this.container = document.createElement('div');
@ -87,7 +88,13 @@ export default class AppSearch {
private scrollable: Scrollable; private scrollable: Scrollable;
constructor(public container: HTMLElement, public searchInput: InputSearch, public searchGroups: {[group in SearchGroupType]: SearchGroup}, public onSearch?: (count: number) => void) { constructor(
public container: HTMLElement,
public searchInput: InputSearch,
public searchGroups: {[group in SearchGroupType]: SearchGroup},
public onSearch?: (count: number) => void,
public noIcons?: boolean
) {
this.scrollable = new Scrollable(this.container); this.scrollable = new Scrollable(this.container);
this.listsContainer = this.scrollable.container as HTMLDivElement; this.listsContainer = this.scrollable.container as HTMLDivElement;
for(const i in this.searchGroups) { for(const i in this.searchGroups) {
@ -200,7 +207,8 @@ export default class AppSearch {
avatarSize: 54, avatarSize: 54,
meAsSaved: false, meAsSaved: false,
message, message,
query query,
noIcons: this.noIcons
}); });
} catch(err) { } catch(err) {
console.error('[appSearch] render search result', err); console.error('[appSearch] render search result', err);

3
src/components/appSearchSuper..ts

@ -1116,7 +1116,8 @@ export default class AppSearchSuper {
container: this.searchGroups.people.list, container: this.searchGroups.people.list,
onlyFirstName: true, onlyFirstName: true,
avatarSize: 54, avatarSize: 54,
autonomous: false autonomous: false,
noIcons: this.searchGroups.people.noIcons
}); });
}); });
} }

33
src/components/chat/bubbles.ts

@ -110,6 +110,7 @@ import noop from '../../helpers/noop';
import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText'; 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';
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([
@ -1047,6 +1048,19 @@ export default class ChatBubbles {
} }
}; };
private stickerEffectObserverCallback = (entry: IntersectionObserverEntry) => {
if(entry.isIntersecting) {
this.observer.unobserve(entry.target, this.stickerEffectObserverCallback);
const attachmentDiv: HTMLElement = entry.target.querySelector('.attachment');
getHeavyAnimationPromise().then(() => {
if(isInDOM(attachmentDiv)) {
simulateClickEvent(attachmentDiv);
}
});
}
};
private createResizeObserver() { private createResizeObserver() {
if(!('ResizeObserver' in window) || this.resizeObserver) { if(!('ResizeObserver' in window) || this.resizeObserver) {
return; return;
@ -2110,6 +2124,8 @@ export default class ChatBubbles {
this.observer.unobserve(bubble, this.viewsObserverCallback); this.observer.unobserve(bubble, this.viewsObserverCallback);
this.viewsMids.delete(mid); this.viewsMids.delete(mid);
this.observer.unobserve(bubble, this.stickerEffectObserverCallback);
} }
if(this.emptyPlaceholderBubble === bubble) { if(this.emptyPlaceholderBubble === bubble) {
@ -3426,16 +3442,14 @@ export default class ChatBubbles {
contentWrapper.append(bubbleContainer); contentWrapper.append(bubbleContainer);
bubble.append(contentWrapper); bubble.append(contentWrapper);
if(!our && !message.pFlags.out && this.observer) { const isInUnread = !our && !message.pFlags.out && (message.pFlags.unread ||
// this.log('not our message', message, message.pFlags.unread);
const isUnread = message.pFlags.unread ||
isMentionUnread(message)/* || isMentionUnread(message)/* ||
(this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid) */; (this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid) */);
if(isUnread) { if(isInUnread && this.observer) {
// this.log('not our message', message, message.pFlags.unread);
this.observer.observe(bubble, this.unreadedObserverCallback); this.observer.observe(bubble, this.unreadedObserverCallback);
this.unreaded.set(bubble, message.mid); this.unreaded.set(bubble, message.mid);
} }
}
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
const ret = { const ret = {
@ -4017,8 +4031,13 @@ export default class ChatBubbles {
emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined, emoji: bubble.classList.contains('emoji-big') ? messageMessage : undefined,
withThumb: true, withThumb: true,
loadPromises, loadPromises,
isOut isOut,
noPremium: messageMedia?.pFlags?.nopremium
}); });
if(isInUnread || isOutgoing/* || true */) {
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 */) {
// this.log('never get free 2', doc); // this.log('never get free 2', doc);

3
src/components/chat/inlineHelper.ts

@ -28,8 +28,9 @@ import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import wrapRichText from '../../lib/richTextProcessor/wrapRichText'; import wrapRichText from '../../lib/richTextProcessor/wrapRichText';
import generateQId from '../../lib/appManagers/utils/inlineBots/generateQId'; import generateQId from '../../lib/appManagers/utils/inlineBots/generateQId';
import appDownloadManager from '../../lib/appManagers/appDownloadManager'; import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {AnimationItemGroup} from '../animationIntersector';
const ANIMATION_GROUP = 'INLINE-HELPER'; const ANIMATION_GROUP: AnimationItemGroup = 'INLINE-HELPER';
// const GRID_ITEMS = 5; // const GRID_ITEMS = 5;
export default class InlineHelper extends AutocompleteHelper { export default class InlineHelper extends AutocompleteHelper {

13
src/components/chat/input.ts

@ -94,6 +94,7 @@ import {modifyAckedPromise} from '../../helpers/modifyAckedResult';
import ChatSendAs from './sendAs'; import ChatSendAs from './sendAs';
import filterAsync from '../../helpers/array/filterAsync'; import filterAsync from '../../helpers/array/filterAsync';
import InputFieldAnimated from '../inputFieldAnimated'; import InputFieldAnimated from '../inputFieldAnimated';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
const RECORD_MIN_TIME = 500; const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -2448,7 +2449,14 @@ export default class ChatInput {
return false; return false;
} }
if(document) { if(!document) {
return false;
}
if(getStickerEffectThumb(document) && !rootScope.premium) {
return false;
}
this.managers.appMessagesManager.sendFile(this.chat.peerId, document, { this.managers.appMessagesManager.sendFile(this.chat.peerId, document, {
...this.chat.getMessageSendingParams(), ...this.chat.getMessageSendingParams(),
isMedia: true, isMedia: true,
@ -2463,9 +2471,6 @@ export default class ChatInput {
return true; return true;
} }
return false;
}
private canToggleHideAuthor() { private canToggleHideAuthor() {
const {forwardElements} = this; const {forwardElements} = this;
if(!forwardElements) return false; if(!forwardElements) return false;

6
src/components/chat/reactionsMenu.ts

@ -19,7 +19,7 @@ import {AppManagers} from '../../lib/appManagers/managers';
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';
import animationIntersector from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import Scrollable, {ScrollableBase, ScrollableX} from '../scrollable'; import Scrollable, {ScrollableBase, ScrollableX} from '../scrollable';
import {wrapSticker} from '../wrappers'; import {wrapSticker} from '../wrappers';
@ -44,7 +44,7 @@ export class ChatReactionsMenu {
public container: HTMLElement; public container: HTMLElement;
private reactionsMap: Map<HTMLElement, ChatReactionsMenuPlayers>; private reactionsMap: Map<HTMLElement, ChatReactionsMenuPlayers>;
public scrollable: ScrollableBase; public scrollable: ScrollableBase;
private animationGroup: string; private animationGroup: AnimationItemGroup;
private middleware: ReturnType<typeof getMiddleware>; private middleware: ReturnType<typeof getMiddleware>;
private message: Message.message; private message: Message.message;
@ -74,7 +74,7 @@ export class ChatReactionsMenu {
// }); // });
this.reactionsMap = new Map(); this.reactionsMap = new Map();
this.animationGroup = 'CHAT-MENU-REACTIONS-' + Date.now(); this.animationGroup = `CHAT-MENU-REACTIONS-${Date.now()}`;
animationIntersector.setOverrideIdleGroup(this.animationGroup, true); animationIntersector.setOverrideIdleGroup(this.animationGroup, true);
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {

4
src/components/emoticonsDropdown/index.ts

@ -7,7 +7,7 @@
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport'; import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import appImManager from '../../lib/appManagers/appImManager'; import appImManager from '../../lib/appManagers/appImManager';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
import animationIntersector from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import {horizontalMenu} from '../horizontalMenu'; import {horizontalMenu} from '../horizontalMenu';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import Scrollable, {ScrollableX} from '../scrollable'; import Scrollable, {ScrollableX} from '../scrollable';
@ -31,7 +31,7 @@ import {AppManagers} from '../../lib/appManagers/managers';
import type LazyLoadQueueIntersector from '../lazyLoadQueueIntersector'; import type LazyLoadQueueIntersector from '../lazyLoadQueueIntersector';
import {simulateClickEvent} from '../../helpers/dom/clickEvent'; import {simulateClickEvent} from '../../helpers/dom/clickEvent';
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; export const EMOTICONSSTICKERGROUP: AnimationItemGroup = 'emoticons-dropdown';
export interface EmoticonsTab { export interface EmoticonsTab {
init: () => void, init: () => void,

76
src/components/emoticonsDropdown/tabs/stickers.ts

@ -5,16 +5,15 @@
*/ */
import emoticonsDropdown, {EmoticonsDropdown, EMOTICONSSTICKERGROUP, EmoticonsTab} from '..'; import emoticonsDropdown, {EmoticonsDropdown, EMOTICONSSTICKERGROUP, EmoticonsTab} from '..';
import findUpAttribute from '../../../helpers/dom/findUpAttribute';
import findUpClassName from '../../../helpers/dom/findUpClassName'; import findUpClassName from '../../../helpers/dom/findUpClassName';
import mediaSizes from '../../../helpers/mediaSizes'; import mediaSizes from '../../../helpers/mediaSizes';
import {MessagesAllStickers, StickerSet} from '../../../layer'; import {MessagesAllStickers, StickerSet} from '../../../layer';
import {MyDocument} from '../../../lib/appManagers/appDocsManager'; import {MyDocument} from '../../../lib/appManagers/appDocsManager';
import {AppManagers} from '../../../lib/appManagers/managers'; import {AppManagers} from '../../../lib/appManagers/managers';
import {i18n} from '../../../lib/langPack'; import {i18n, LangPackKey} from '../../../lib/langPack';
import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText'; import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText';
import rootScope from '../../../lib/rootScope'; import rootScope from '../../../lib/rootScope';
import animationIntersector from '../../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../../animationIntersector';
import LazyLoadQueue from '../../lazyLoadQueue'; import LazyLoadQueue from '../../lazyLoadQueue';
import LazyLoadQueueRepeat from '../../lazyLoadQueueRepeat'; import LazyLoadQueueRepeat from '../../lazyLoadQueueRepeat';
import {putPreloader} from '../../putPreloader'; import {putPreloader} from '../../putPreloader';
@ -36,7 +35,7 @@ export class SuperStickerRenderer {
constructor( constructor(
private regularLazyLoadQueue: LazyLoadQueue, private regularLazyLoadQueue: LazyLoadQueue,
private group: string, private group: AnimationItemGroup,
private managers: AppManagers, private managers: AppManagers,
private options?: IntersectionObserverInit private options?: IntersectionObserverInit
) { ) {
@ -117,7 +116,8 @@ export class SuperStickerRenderer {
group: this.group, group: this.group,
onlyThumb: false, onlyThumb: false,
play: true, play: true,
loop: true loop: true,
withLock: true
}).then(({render}) => render); }).then(({render}) => render);
promise.then(() => { promise.then(() => {
@ -157,7 +157,8 @@ type StickersTabCategory = {
items: { items: {
document: MyDocument, document: MyDocument,
element: HTMLElement element: HTMLElement
}[] }[],
pos?: number
}; };
const RECENT_STICKERS_COUNT = 20; const RECENT_STICKERS_COUNT = 20;
@ -168,6 +169,7 @@ export default class StickersTab implements EmoticonsTab {
private categories: {[id: string]: StickersTabCategory}; private categories: {[id: string]: StickersTabCategory};
private categoriesMap: Map<HTMLElement, StickersTabCategory>; private categoriesMap: Map<HTMLElement, StickersTabCategory>;
private categoriesIntersector: VisibilityIntersector; private categoriesIntersector: VisibilityIntersector;
private localCategoryIndex: number;
private scroll: Scrollable; private scroll: Scrollable;
private menu: HTMLElement; private menu: HTMLElement;
@ -178,6 +180,7 @@ export default class StickersTab implements EmoticonsTab {
constructor(private managers: AppManagers) { constructor(private managers: AppManagers) {
this.categories = {}; this.categories = {};
this.categoriesMap = new Map(); this.categoriesMap = new Map();
this.localCategoryIndex = 0;
} }
private createCategory(stickerSet: StickerSet.stickerSet, _title: HTMLElement | DocumentFragment) { private createCategory(stickerSet: StickerSet.stickerSet, _title: HTMLElement | DocumentFragment) {
@ -264,7 +267,8 @@ export default class StickersTab implements EmoticonsTab {
const category = this.createCategory(set, wrapEmojiText(set.title)); const category = this.createCategory(set, wrapEmojiText(set.title));
const {menuTab, menuTabPadding, container} = category.elements; const {menuTab, menuTabPadding, container} = category.elements;
positionElementByIndex(menuTab, this.menu, prepend ? 1 : 0xFFFF); const pos = prepend ? this.localCategoryIndex : 0xFFFF;
positionElementByIndex(menuTab, this.menu, pos);
const promise = this.managers.appStickersManager.getStickerSet(set); const promise = this.managers.appStickersManager.getStickerSet(set);
this.categoryAppendStickers( this.categoryAppendStickers(
@ -273,7 +277,7 @@ export default class StickersTab implements EmoticonsTab {
); );
// const stickerSet = await promise; // const stickerSet = await promise;
positionElementByIndex(container, this.scroll.container, prepend ? 1 : 0xFFFF, -1); positionElementByIndex(container, this.scroll.container, pos, -1);
wrapStickerSetThumb({ wrapStickerSetThumb({
set, set,
@ -367,11 +371,18 @@ export default class StickersTab implements EmoticonsTab {
const preloader = putPreloader(this.content, true); const preloader = putPreloader(this.content, true);
const recentCategory = this.createCategory({id: 'recent'} as any, i18n('Stickers.Recent')); const createLocalCategory = (id: string, title: LangPackKey, icon?: string) => {
recentCategory.elements.title.classList.add('disable-hover'); const category = this.createCategory({id} as any, i18n(title));
recentCategory.elements.menuTab.classList.add('tgico-recent', 'active'); category.elements.title.classList.add('disable-hover');
recentCategory.elements.menuTabPadding.remove(); icon && category.elements.menuTab.classList.add('tgico-' + icon);
this.toggleRecentCategory(recentCategory, false); category.elements.menuTabPadding.remove();
category.pos = this.localCategoryIndex++;
this.toggleLocalCategory(category, false);
return category;
};
const recentCategory = createLocalCategory('recent', 'Stickers.Recent', 'recent');
recentCategory.elements.menuTab.classList.add('active');
const clearButton = ButtonIcon('close', {noRipple: true}); const clearButton = ButtonIcon('close', {noRipple: true});
recentCategory.elements.title.append(clearButton); recentCategory.elements.title.append(clearButton);
@ -391,24 +402,42 @@ export default class StickersTab implements EmoticonsTab {
const sliced = stickers.slice(0, RECENT_STICKERS_COUNT) as MyDocument[]; const sliced = stickers.slice(0, RECENT_STICKERS_COUNT) as MyDocument[];
clearCategoryItems(recentCategory); clearCategoryItems(recentCategory);
this.toggleRecentCategory(recentCategory, !!sliced.length); this.toggleLocalCategory(recentCategory, !!sliced.length);
this.categoryAppendStickers(recentCategory, Promise.resolve(sliced)); this.categoryAppendStickers(recentCategory, Promise.resolve(sliced));
}; };
Promise.all([ const premiumCategory = createLocalCategory('premium', 'PremiumStickersShort');
const s = document.createElement('span');
s.classList.add('tgico-star', 'color-premium');
premiumCategory.elements.menuTab.append(s);
const promises = [
this.managers.appStickersManager.getRecentStickers().then((stickers) => { this.managers.appStickersManager.getRecentStickers().then((stickers) => {
preloader.remove();
onRecentStickers(stickers.stickers as MyDocument[]); onRecentStickers(stickers.stickers as MyDocument[]);
}), }),
this.managers.appStickersManager.getAllStickers().then((res) => { this.managers.appStickersManager.getAllStickers().then((res) => {
preloader.remove();
for(const set of (res as MessagesAllStickers.messagesAllStickers).sets) { for(const set of (res as MessagesAllStickers.messagesAllStickers).sets) {
this.renderStickerSet(set); this.renderStickerSet(set);
} }
}),
this.managers.appStickersManager.getPremiumStickers().then((stickers) => {
const length = stickers.length;
this.toggleLocalCategory(premiumCategory, rootScope.premium && !!length);
this.categoryAppendStickers(premiumCategory, Promise.resolve(stickers));
rootScope.addEventListener('premium_toggle', (isPremium) => {
this.toggleLocalCategory(this.categories['premium'], isPremium && !!length);
});
}) })
]).finally(() => { ];
Promise.race(promises).finally(() => {
preloader.remove();
});
Promise.all(promises).finally(() => {
this.mounted = true; this.mounted = true;
setTyping(); setTyping();
setActive(0); setActive(0);
@ -482,13 +511,14 @@ export default class StickersTab implements EmoticonsTab {
this.init = null; this.init = null;
} }
private toggleRecentCategory(category: StickersTabCategory, visible: boolean) { private toggleLocalCategory(category: StickersTabCategory, visible: boolean) {
if(!visible) { if(!visible) {
category.elements.menuTab.remove(); category.elements.menuTab.remove();
category.elements.container.remove(); category.elements.container.remove();
} else { } else {
positionElementByIndex(category.elements.menuTab, this.menu, 0); const pos = category.pos;
positionElementByIndex(category.elements.container, this.scroll.container, 0); positionElementByIndex(category.elements.menuTab, this.menu, pos);
positionElementByIndex(category.elements.container, this.scroll.container, pos);
} }
// category.elements.container.classList.toggle('hide', !visible); // category.elements.container.classList.toggle('hide', !visible);
@ -518,7 +548,7 @@ export default class StickersTab implements EmoticonsTab {
} }
this.setCategoryItemsHeight(category); this.setCategoryItemsHeight(category);
this.toggleRecentCategory(category, true); this.toggleLocalCategory(category, true);
} }
onClose() { onClose() {

4
src/components/gifsMasonry.ts

@ -6,7 +6,7 @@
import type {MyDocument} from '../lib/appManagers/appDocsManager'; import type {MyDocument} from '../lib/appManagers/appDocsManager';
import {wrapVideo} from './wrappers'; import {wrapVideo} from './wrappers';
import animationIntersector from './animationIntersector'; import animationIntersector, {AnimationItemGroup} from './animationIntersector';
import Scrollable from './scrollable'; import Scrollable from './scrollable';
import deferredPromise, {CancellablePromise} from '../helpers/cancellablePromise'; import deferredPromise, {CancellablePromise} from '../helpers/cancellablePromise';
import renderImageFromUrl from '../helpers/dom/renderImageFromUrl'; import renderImageFromUrl from '../helpers/dom/renderImageFromUrl';
@ -28,7 +28,7 @@ export default class GifsMasonry {
constructor( constructor(
private element: HTMLElement, private element: HTMLElement,
private group: string, private group: AnimationItemGroup,
private scrollable: Scrollable, private scrollable: Scrollable,
attach = true attach = true
) { ) {

7
src/components/popups/stickers.ts

@ -9,7 +9,7 @@ import type {AppStickersManager} from '../../lib/appManagers/appStickersManager'
import {wrapSticker} from '../wrappers'; import {wrapSticker} from '../wrappers';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import {putPreloader} from '../putPreloader'; import {putPreloader} from '../putPreloader';
import animationIntersector from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import appImManager from '../../lib/appManagers/appImManager'; import appImManager from '../../lib/appManagers/appImManager';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import {i18n} from '../../lib/langPack'; import {i18n} from '../../lib/langPack';
@ -21,7 +21,7 @@ import {toastNew} from '../toast';
import setInnerHTML from '../../helpers/dom/setInnerHTML'; import setInnerHTML from '../../helpers/dom/setInnerHTML';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText'; import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
const ANIMATION_GROUP = 'STICKERS-POPUP'; const ANIMATION_GROUP: AnimationItemGroup = 'STICKERS-POPUP';
export default class PopupStickers extends PopupElement { export default class PopupStickers extends PopupElement {
private stickersFooter: HTMLElement; private stickersFooter: HTMLElement;
@ -126,7 +126,8 @@ export default class PopupStickers extends PopupElement {
play: true, play: true,
loop: true, loop: true,
width: size, width: size,
height: size height: size,
withLock: true
}); });
return div; return div;

5
src/components/scrollable.ts

@ -9,6 +9,7 @@ import {logger, LogTypes} from '../lib/logger';
import fastSmoothScroll, {ScrollOptions} from '../helpers/fastSmoothScroll'; import fastSmoothScroll, {ScrollOptions} from '../helpers/fastSmoothScroll';
import useHeavyAnimationCheck from '../hooks/useHeavyAnimationCheck'; import useHeavyAnimationCheck from '../hooks/useHeavyAnimationCheck';
import cancelEvent from '../helpers/dom/cancelEvent'; import cancelEvent from '../helpers/dom/cancelEvent';
import {IS_ANDROID} from '../environment/userAgent';
/* /*
var el = $0; var el = $0;
var height = 0; var height = 0;
@ -52,6 +53,8 @@ const scrollsIntersector = new IntersectionObserver((entries) => {
} }
}); */ }); */
const SCROLL_THROTTLE = IS_ANDROID ? 200 : 24;
export class ScrollableBase { export class ScrollableBase {
protected log: ReturnType<typeof logger>; protected log: ReturnType<typeof logger>;
@ -196,7 +199,7 @@ export class ScrollableBase {
this.checkForTriggers(); this.checkForTriggers();
} }
// }); // });
}, 200); }, SCROLL_THROTTLE);
}; };
public cancelMeasure() { public cancelMeasure() {

2
src/components/sidebarLeft/index.ts

@ -367,7 +367,7 @@ export class AppSidebarLeft extends SidebarSlider {
contacts: new SearchGroup('SearchAllChatsShort', 'contacts', undefined, undefined, undefined, undefined, close), contacts: new SearchGroup('SearchAllChatsShort', 'contacts', undefined, undefined, undefined, undefined, close),
globalContacts: new SearchGroup('GlobalSearch', 'contacts', undefined, undefined, undefined, undefined, close), globalContacts: new SearchGroup('GlobalSearch', 'contacts', undefined, undefined, undefined, undefined, close),
messages: new SearchGroup('SearchMessages', 'messages'), messages: new SearchGroup('SearchMessages', 'messages'),
people: new SearchGroup(false, 'contacts', true, 'search-group-people', true, false, close), people: new SearchGroup(false, 'contacts', true, 'search-group-people', true, false, close, true),
recent: new SearchGroup('Recent', 'contacts', true, 'search-group-recent', true, true, close) recent: new SearchGroup('Recent', 'contacts', true, 'search-group-recent', true, true, close)
}; };

4
src/components/sidebarRight/tabs/gifs.ts

@ -6,7 +6,7 @@
import {SliderSuperTab} from '../../slider'; import {SliderSuperTab} from '../../slider';
import InputSearch from '../../inputSearch'; import InputSearch from '../../inputSearch';
import animationIntersector from '../../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../../animationIntersector';
import appSidebarRight from '..'; import appSidebarRight from '..';
import {AppInlineBotsManager} from '../../../lib/appManagers/appInlineBotsManager'; import {AppInlineBotsManager} from '../../../lib/appManagers/appInlineBotsManager';
import GifsMasonry from '../../gifsMasonry'; import GifsMasonry from '../../gifsMasonry';
@ -17,7 +17,7 @@ import findUpClassName from '../../../helpers/dom/findUpClassName';
import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import {NULL_PEER_ID} from '../../../lib/mtproto/mtproto_config'; import {NULL_PEER_ID} from '../../../lib/mtproto/mtproto_config';
const ANIMATIONGROUP = 'GIFS-SEARCH'; const ANIMATIONGROUP: AnimationItemGroup = 'GIFS-SEARCH';
export default class AppGifsTab extends SliderSuperTab { export default class AppGifsTab extends SliderSuperTab {
private inputSearch: InputSearch; private inputSearch: InputSearch;

3
src/components/sidebarRight/tabs/stickers.ts

@ -151,7 +151,8 @@ export default class AppStickersTab extends SliderSuperTab {
play: true, play: true,
loop: true, loop: true,
width: 68, width: 68,
height: 68 height: 68,
withLock: true
}); });
} }
}); });

143
src/components/wrappers/sticker.ts

@ -8,6 +8,7 @@ import IS_WEBP_SUPPORTED from '../../environment/webpSupport';
import assumeType from '../../helpers/assumeType'; import assumeType from '../../helpers/assumeType';
import getPathFromBytes from '../../helpers/bytes/getPathFromBytes'; import getPathFromBytes from '../../helpers/bytes/getPathFromBytes';
import deferredPromise from '../../helpers/cancellablePromise'; import deferredPromise from '../../helpers/cancellablePromise';
import computeLockColor from '../../helpers/computeLockColor';
import cancelEvent from '../../helpers/dom/cancelEvent'; import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent} from '../../helpers/dom/clickEvent'; import {attachClickEvent} from '../../helpers/dom/clickEvent';
import createVideo from '../../helpers/dom/createVideo'; import createVideo from '../../helpers/dom/createVideo';
@ -34,7 +35,7 @@ import type {ThumbCache} from '../../lib/storages/thumbs';
import webpWorkerController from '../../lib/webp/webpWorkerController'; import webpWorkerController from '../../lib/webp/webpWorkerController';
import {SendMessageEmojiInteractionData} from '../../types'; import {SendMessageEmojiInteractionData} from '../../types';
import {getEmojiToneIndex} from '../../vendor/emoji'; import {getEmojiToneIndex} from '../../vendor/emoji';
import animationIntersector from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import PopupStickers from '../popups/stickers'; import PopupStickers from '../popups/stickers';
import {hideToast, toastNew} from '../toast'; import {hideToast, toastNew} from '../toast';
@ -44,12 +45,14 @@ import wrapStickerAnimation from './stickerAnimation';
const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2; const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2;
const EMOJI_EFFECT_MULTIPLIER = 3; 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}: { 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}: {
doc: MyDocument, doc: MyDocument,
div: HTMLElement, div: HTMLElement,
middleware?: () => boolean, middleware?: () => boolean,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
group?: string, group?: AnimationItemGroup,
play?: boolean, play?: boolean,
onlyThumb?: boolean, onlyThumb?: boolean,
emoji?: string, emoji?: string,
@ -64,7 +67,9 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
static?: boolean, static?: boolean,
managers?: AppManagers, managers?: AppManagers,
fullThumb?: PhotoSize | VideoSize, fullThumb?: PhotoSize | VideoSize,
isOut?: boolean isOut?: boolean,
noPremium?: boolean,
withLock?: boolean
}) { }) {
const stickerType = doc.sticker; const stickerType = doc.sticker;
if(stickerType === 1) { if(stickerType === 1) {
@ -134,6 +139,13 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
div.classList.add('reflect-x'); div.classList.add('reflect-x');
} }
const willHaveLock = effectThumb && withLock;
if(willHaveLock) {
div.classList.add('is-premium-sticker', 'tgico-premium_lock');
const lockUrl = locksUrls[doc.id];
lockUrl && div.style.setProperty('--lock-url', `url(${lockUrl})`);
}
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);
@ -331,6 +343,11 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
// const deferred = deferredPromise<void>(); // const deferred = deferredPromise<void>();
const setLockColor = willHaveLock ? () => {
const lockUrl = locksUrls[doc.id] ??= computeLockColor(animation.canvas);
div.style.setProperty('--lock-url', `url(${lockUrl})`);
} : undefined;
animation.addEventListener('firstFrame', () => { animation.addEventListener('firstFrame', () => {
const element = div.firstElementChild; const element = div.firstElementChild;
if(needFadeIn !== false) { if(needFadeIn !== false) {
@ -367,6 +384,10 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
saveLottiePreview(doc, animation.canvas, toneIndex); saveLottiePreview(doc, animation.canvas, toneIndex);
} }
if(willHaveLock) {
setLockColor();
}
// deferred.resolve(); // deferred.resolve();
}, {once: true}); }, {once: true});
@ -473,54 +494,6 @@ 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;
@ -633,5 +606,71 @@ export default async function wrapSticker({doc, div, middleware, lazyLoadQueue,
} }
} }
if(stickerType === 2 && effectThumb && isOut !== undefined && !noPremium) {
attachStickerEffectHandler({
container: div,
doc,
managers,
middleware,
isOut,
width,
loadPromise
});
}
return {render: loadPromise}; return {render: loadPromise};
} }
function attachStickerEffectHandler({container, doc, managers, middleware, isOut, width, loadPromise}: {
container: HTMLElement,
doc: MyDocument,
managers: AppManagers,
middleware: () => boolean,
isOut: boolean,
width: number,
loadPromise: Promise<any>
}) {
managers.appStickersManager.preloadSticker(doc.id, true);
let playing = false;
attachClickEvent(container, 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;
await loadPromise;
const {animationDiv, stickerPromise} = wrapStickerAnimation({
doc,
middleware,
side: isOut ? 'right' : 'left',
size: width * STICKER_EFFECT_MULTIPLIER,
target: container,
play: true,
fullThumb: getStickerEffectThumb(doc)
});
if(isOut !== undefined && !isOut) {
animationDiv.classList.add('reflect-x');
}
stickerPromise.then((player) => {
player.addEventListener('destroy', () => {
playing = false;
});
});
});
}

26
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 windowSize from '../../helpers/windowSize';
import {PhotoSize, VideoSize} from '../../layer'; 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';
@ -45,6 +46,13 @@ export default function wrapStickerAnimation({
animationDiv.style.width = size + 'px'; animationDiv.style.width = size + 'px';
animationDiv.style.height = size + 'px'; animationDiv.style.height = size + 'px';
let animation: RLottiePlayer;
const unmountAnimation = () => {
animation?.remove();
animationDiv.remove();
appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll);
};
const stickerPromise = wrapSticker({ const stickerPromise = wrapSticker({
div: animationDiv, div: animationDiv,
doc, doc,
@ -59,13 +67,12 @@ export default function wrapStickerAnimation({
skipRatio, skipRatio,
managers, managers,
fullThumb fullThumb
}).then(({render}) => render).then((animation) => { }).then(({render}) => render).then((_animation) => {
assumeType<RLottiePlayer>(animation); assumeType<RLottiePlayer>(_animation);
animation = _animation;
animation.addEventListener('enterFrame', (frameNo) => { animation.addEventListener('enterFrame', (frameNo) => {
if(frameNo === animation.maxFrame) { if(frameNo === animation.maxFrame || !isInDOM(target)) {
animation.remove(); unmountAnimation();
animationDiv.remove();
appImManager.chat.bubbles.scrollable.container.removeEventListener('scroll', onScroll);
} }
}); });
@ -88,6 +95,7 @@ export default function wrapStickerAnimation({
const stableOffsetX = /* size / 8 */16 * (side === 'right' ? 1 : -1); const stableOffsetX = /* size / 8 */16 * (side === 'right' ? 1 : -1);
const setPosition = () => { const setPosition = () => {
if(!isInDOM(target)) { if(!isInDOM(target)) {
unmountAnimation();
return; return;
} }
@ -104,6 +112,12 @@ export default function wrapStickerAnimation({
// const y = rect.bottom - size + size / 4; // const y = rect.bottom - size + size / 4;
const y = rect.top + ((rect.height - size) / 2) + (side === 'center' ? 0 : randomOffsetY); const y = rect.top + ((rect.height - size) / 2) + (side === 'center' ? 0 : randomOffsetY);
// animationDiv.style.transform = `translate(${x}px, ${y}px)`; // animationDiv.style.transform = `translate(${x}px, ${y}px)`;
if(y <= -size || y >= windowSize.height) {
unmountAnimation();
return;
}
animationDiv.style.top = y + 'px'; animationDiv.style.top = y + 'px';
animationDiv.style.left = x + 'px'; animationDiv.style.left = x + 'px';
}; };

4
src/components/wrappers/stickerSetThumb.ts

@ -11,7 +11,7 @@ import appDownloadManager from '../../lib/appManagers/appDownloadManager';
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import lottieLoader from '../../lib/rlottie/lottieLoader'; import lottieLoader from '../../lib/rlottie/lottieLoader';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
import animationIntersector from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import wrapSticker from './sticker'; import wrapSticker from './sticker';
@ -19,7 +19,7 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container
set: StickerSet.stickerSet, set: StickerSet.stickerSet,
lazyLoadQueue: LazyLoadQueue, lazyLoadQueue: LazyLoadQueue,
container: HTMLElement, container: HTMLElement,
group: string, group: AnimationItemGroup,
autoplay: boolean, autoplay: boolean,
width: number, width: number,
height: number, height: number,

4
src/components/wrappers/video.ts

@ -27,7 +27,7 @@ import {AppManagers} from '../../lib/appManagers/managers';
import {NULL_PEER_ID} from '../../lib/mtproto/mtproto_config'; import {NULL_PEER_ID} from '../../lib/mtproto/mtproto_config';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
import {ThumbCache} from '../../lib/storages/thumbs'; import {ThumbCache} from '../../lib/storages/thumbs';
import animationIntersector from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import appMediaPlaybackController, {MediaSearchContext} from '../appMediaPlaybackController'; import appMediaPlaybackController, {MediaSearchContext} from '../appMediaPlaybackController';
import {findMediaTargets} from '../audio'; import {findMediaTargets} from '../audio';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
@ -71,7 +71,7 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
noInfo?: boolean, noInfo?: boolean,
noPlayButton?: boolean, noPlayButton?: boolean,
group?: string, group?: AnimationItemGroup,
onlyPreview?: boolean, onlyPreview?: boolean,
withoutPreloader?: boolean, withoutPreloader?: boolean,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],

3
src/config/app.ts

@ -25,7 +25,8 @@ const App = {
domains: [MAIN_DOMAIN] as string[], domains: [MAIN_DOMAIN] as string[],
baseDcId: 2 as DcId, baseDcId: 2 as DcId,
isMainDomain: location.hostname === MAIN_DOMAIN, isMainDomain: location.hostname === MAIN_DOMAIN,
suffix: 'K' suffix: 'K',
cryptoWorkers: 4
}; };
if(App.isMainDomain) { // use Webogram credentials then if(App.isMainDomain) { // use Webogram credentials then

28
src/helpers/averageColor.ts

@ -11,28 +11,27 @@ export function averageColorFromCanvas(canvas: HTMLCanvasElement) {
const pixel = new Array(4).fill(0); const pixel = new Array(4).fill(0);
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data; const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
const pixelsLength = pixels.length / 4;
for(let i = 0; i < pixels.length; i += 4) { for(let i = 0; i < pixels.length; i += 4) {
pixel[0] += pixels[i]; // const alphaPixel = pixels[i + 3];
pixel[1] += pixels[i + 1]; pixel[0] += pixels[i]/* * (alphaPixel / 255) */;
pixel[2] += pixels[i + 2]; pixel[1] += pixels[i + 1]/* * (alphaPixel / 255) */;
pixel[2] += pixels[i + 2]/* * (alphaPixel / 255) */;
pixel[3] += pixels[i + 3]; pixel[3] += pixels[i + 3];
} }
const pixelsLength = pixels.length / 4;
const outPixel = new Uint8ClampedArray(4); const outPixel = new Uint8ClampedArray(4);
outPixel[0] = pixel[0] / pixelsLength; outPixel[0] = pixel[0] / pixelsLength;
outPixel[1] = pixel[1] / pixelsLength; outPixel[1] = pixel[1] / pixelsLength;
outPixel[2] = pixel[2] / pixelsLength; outPixel[2] = pixel[2] / pixelsLength;
outPixel[3] = pixel[3] / pixelsLength; outPixel[3] = pixel[3] / pixelsLength;
// outPixel[3] = 255;
return outPixel; return outPixel;
} }
export function averageColor(imageUrl: string) { export function averageColorFromImageSource(imageSource: CanvasImageSource, width: number, height: number) {
const img = document.createElement('img');
return new Promise<Uint8ClampedArray>((resolve) => {
renderImageFromUrl(img, imageUrl, () => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ratio = img.naturalWidth / img.naturalHeight; const ratio = width / height;
const DIMENSIONS = 50; const DIMENSIONS = 50;
if(ratio === 1) { if(ratio === 1) {
canvas.width = DIMENSIONS; canvas.width = DIMENSIONS;
@ -45,8 +44,15 @@ export function averageColor(imageUrl: string) {
} }
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height); context.drawImage(imageSource, 0, 0, width, height, 0, 0, canvas.width, canvas.height);
resolve(averageColorFromCanvas(canvas)); return averageColorFromCanvas(canvas);
}
export function averageColor(imageUrl: string) {
const img = document.createElement('img');
return new Promise<Uint8ClampedArray>((resolve) => {
renderImageFromUrl(img, imageUrl, () => {
resolve(averageColorFromImageSource(img, img.naturalWidth, img.naturalHeight));
}); });
}); });
}; };

31
src/helpers/computeLockColor.ts

@ -0,0 +1,31 @@
// https://github.com/telegramdesktop/tdesktop/blob/543bfab24a76402992421063f1e6444f347d31fe/Telegram/SourceFiles/boxes/sticker_set_box.cpp#L75
export default function computeLockColor(canvas: HTMLCanvasElement) {
const context = canvas.getContext('2d');
const size = 20 * (canvas.dpr ?? 1);
const width = size;
const height = size;
const skipx = (canvas.width - width) / 2;
const margin = 0/* * (canvas.dpr ?? 1) */;
const skipy = canvas.height - height - margin;
const imageData = context.getImageData(skipx, skipy, width, height).data;
let sr = 0, sg = 0, sb = 0, sa = 0;
for(let i = 0; i < imageData.length; i += 4) {
sr += imageData[i];
sg += imageData[i + 1];
sb += imageData[i + 2];
sa += imageData[i + 3];
}
const outCanvas = document.createElement('canvas');
outCanvas.width = size;
outCanvas.height = size;
const outContext = outCanvas.getContext('2d');
const color = new Uint8ClampedArray([sr * 255 / sa, sg * 255 / sa, sb * 255 / sa, 255]);
const rgba = `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;
outContext.fillStyle = rgba;
outContext.fillRect(0, 0, outCanvas.width, outCanvas.height);
outContext.fillStyle = `rgba(112, 117, 121, 0.3)`;
outContext.fillRect(0, 0, outCanvas.width, outCanvas.height);
// document.querySelector('.popup-title').append(c);
return outCanvas.toDataURL('image/jpeg');
}

2
src/helpers/fileName.ts

@ -58,7 +58,7 @@ export function getFileNameByLocation(location: InputFileLocation | InputWebFile
} }
} }
return str + (options.downloadId ? '_download' : '') + (ext ? '.' + ext : ext); return str + (options?.downloadId ? '_download' : '') + (ext ? '.' + ext : ext);
} }
export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download'; export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download';

3
src/helpers/overlayClickHandler.ts

@ -32,7 +32,10 @@ export default class OverlayClickHandler extends EventListenerBase<{
return; return;
} }
if(this.listenerOptions?.capture) {
cancelEvent(e); cancelEvent(e);
}
this.close(); this.close();
}; };

9
src/helpers/setWorkerProxy.ts

@ -8,8 +8,10 @@ export default function setWorkerProxy() {
// * hook worker constructor to set search parameters (test, debug, etc) // * hook worker constructor to set search parameters (test, debug, etc)
const workerHandler = { const workerHandler = {
construct(target: any, args: any) { construct(target: any, args: any) {
// console.log(target, args); let url = args[0] + '';
const url = args[0] + location.search; if(url.indexOf('blob:') !== 0) {
url += location.search;
}
return new target(url); return new target(url);
} }
@ -18,8 +20,7 @@ export default function setWorkerProxy() {
[ [
Worker, Worker,
typeof(SharedWorker) !== 'undefined' && SharedWorker typeof(SharedWorker) !== 'undefined' && SharedWorker
].forEach((w) => { ].filter(Boolean).forEach((w) => {
if(!w) return;
window[w.name as any] = new Proxy(w, workerHandler); window[w.name as any] = new Proxy(w, workerHandler);
}); });
} }

1
src/lang.ts

@ -748,6 +748,7 @@ const lang = {
'PaymentCheckoutName': 'Name', 'PaymentCheckoutName': 'Name',
'ClearRecentStickersAlertTitle': 'Clear recent stickers', 'ClearRecentStickersAlertTitle': 'Clear recent stickers',
'ClearRecentStickersAlertMessage': 'Do you want to clear all your recent stickers?', 'ClearRecentStickersAlertMessage': 'Do you want to clear all your recent stickers?',
'PremiumStickersShort': 'Premium',
// * macos // * macos
'AccountSettings.Filters': 'Chat Folders', 'AccountSettings.Filters': 'Chat Folders',

10
src/lib/appManagers/appDialogsManager.ts

@ -2028,9 +2028,10 @@ export class AppDialogsManager {
autonomous?: boolean, autonomous?: boolean,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
fromName?: string fromName?: string,
noIcons?: boolean
}) { }) {
return this.addDialog(options.peerId, options.container, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous, options.lazyLoadQueue, options.loadPromises, options.fromName); return this.addDialog(options.peerId, options.container, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous, options.lazyLoadQueue, options.loadPromises, options.fromName, options.noIcons);
} }
public addDialog( public addDialog(
@ -2044,7 +2045,8 @@ export class AppDialogsManager {
autonomous = !!container, autonomous = !!container,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
fromName?: string fromName?: string,
noIcons?: boolean
) { ) {
// const dialog = await this.getDialog(_dialog); // const dialog = await this.getDialog(_dialog);
const avatarEl = new AvatarElement(); const avatarEl = new AvatarElement();
@ -2070,7 +2072,7 @@ export class AppDialogsManager {
dialog: meAsSaved, dialog: meAsSaved,
onlyFirstName, onlyFirstName,
plainText: false, plainText: false,
withIcons: true withIcons: !noIcons
}); });
if(loadPromises) { if(loadPromises) {

6
src/lib/appManagers/appImManager.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import animationIntersector from '../../components/animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../../components/animationIntersector';
import appSidebarLeft, {LEFT_COLUMN_ACTIVE_CLASSNAME} from '../../components/sidebarLeft'; import appSidebarLeft, {LEFT_COLUMN_ACTIVE_CLASSNAME} from '../../components/sidebarLeft';
import appSidebarRight, {RIGHT_COLUMN_ACTIVE_CLASSNAME} from '../../components/sidebarRight'; import appSidebarRight, {RIGHT_COLUMN_ACTIVE_CLASSNAME} from '../../components/sidebarRight';
import mediaSizes, {ScreenSize} from '../../helpers/mediaSizes'; import mediaSizes, {ScreenSize} from '../../helpers/mediaSizes';
@ -93,7 +93,7 @@ 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';
export const CHAT_ANIMATION_GROUP = 'chat'; export const CHAT_ANIMATION_GROUP: AnimationItemGroup = 'chat';
export type ChatSavedPosition = { export type ChatSavedPosition = {
mids: number[], mids: number[],
@ -415,6 +415,8 @@ export class AppImManager extends EventListenerBase<{
this.addEventListener('peer_changed', async(peerId) => { this.addEventListener('peer_changed', async(peerId) => {
document.body.classList.toggle('has-chat', !!peerId); document.body.classList.toggle('has-chat', !!peerId);
this.emojiAnimationContainer.textContent = '';
this.overrideHash(peerId); this.overrideHash(peerId);
apiManagerProxy.updateTabState('chatPeerIds', this.chats.map((chat) => chat.peerId).filter(Boolean)); apiManagerProxy.updateTabState('chatPeerIds', this.chats.map((chat) => chat.peerId).filter(Boolean));

28
src/lib/appManagers/appManagersManager.ts

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import App from '../../config/app';
import callbackify from '../../helpers/callbackify'; import callbackify from '../../helpers/callbackify';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import cryptoMessagePort from '../crypto/cryptoMessagePort'; import cryptoMessagePort from '../crypto/cryptoMessagePort';
@ -16,10 +17,13 @@ type Managers = Awaited<ReturnType<typeof createManagers>>;
export class AppManagersManager { export class AppManagersManager {
private managers: Managers | Promise<Managers>; private managers: Managers | Promise<Managers>;
private cryptoPortAttached: boolean; private cryptoWorkersURLs: string[];
private cryptoPortsAttached: number;
private cryptoPortPromise: CancellablePromise<void>; private cryptoPortPromise: CancellablePromise<void>;
constructor() { constructor() {
this.cryptoWorkersURLs = [];
this.cryptoPortsAttached = 0;
this.cryptoPortPromise = deferredPromise(); this.cryptoPortPromise = deferredPromise();
this.cryptoPortPromise.then(() => { this.cryptoPortPromise.then(() => {
this.cryptoPortPromise = undefined; this.cryptoPortPromise = undefined;
@ -38,14 +42,28 @@ export class AppManagersManager {
}); });
port.addEventListener('cryptoPort', (payload, source, event) => { port.addEventListener('cryptoPort', (payload, source, event) => {
if(this.cryptoPortAttached) { const port = event.ports[0];
if(this.cryptoPortsAttached >= this.cryptoWorkersURLs.length) {
port.close();
return; return;
} }
this.cryptoPortAttached = true; ++this.cryptoPortsAttached;
const port = event.ports[0];
cryptoMessagePort.attachPort(port); cryptoMessagePort.attachPort(port);
this.cryptoPortPromise.resolve(); this.cryptoPortPromise?.resolve();
return;
});
port.addEventListener('createProxyWorkerURLs', (blob) => {
const length = this.cryptoWorkersURLs.length;
const maxLength = App.cryptoWorkers;
if(length) {
return this.cryptoWorkersURLs;
}
const newURLs = new Array(maxLength - length).fill(undefined).map(() => URL.createObjectURL(blob));
this.cryptoWorkersURLs.push(...newURLs);
return newURLs;
}); });
} }

10
src/lib/appManagers/appMessagesManager.ts

@ -60,6 +60,7 @@ import MTProtoMessagePort from '../mtproto/mtprotoMessagePort';
import getAlbumText from './utils/messages/getAlbumText'; import getAlbumText from './utils/messages/getAlbumText';
import pause from '../../helpers/schedulers/pause'; import pause from '../../helpers/schedulers/pause';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import getStickerEffectThumb from './utils/stickers/getStickerEffectThumb';
// console.trace('include'); // console.trace('include');
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
@ -2091,7 +2092,7 @@ export class AppMessagesManager extends AppManager {
keys.forEach((key) => { keys.forEach((key) => {
// @ts-ignore // @ts-ignore
message[key] = originalMessage[key]; message[key] = copy(originalMessage[key]);
}); });
const document = (message.media as MessageMedia.messageMediaDocument)?.document as MyDocument; const document = (message.media as MessageMedia.messageMediaDocument)?.document as MyDocument;
@ -2100,6 +2101,13 @@ export class AppMessagesManager extends AppManager {
if(types.includes(document.type)) { if(types.includes(document.type)) {
(message as MyMessage).pFlags.media_unread = true; (message as MyMessage).pFlags.media_unread = true;
} }
if(document.sticker && !this.rootScope.premium) {
const effectThumb = getStickerEffectThumb(document);
if(effectThumb) {
(message.media as MessageMedia.messageMediaDocument).pFlags.nopremium = true;
}
}
} }
if(originalMessage.grouped_id) { if(originalMessage.grouped_id) {

12
src/lib/appManagers/appStickersManager.ts

@ -40,9 +40,9 @@ export class AppStickersManager extends AppManager {
private storage = new AppStorage<Record<Long, MyMessagesStickerSet>, typeof DATABASE_STATE>(DATABASE_STATE, 'stickerSets'); private storage = new AppStorage<Record<Long, MyMessagesStickerSet>, typeof DATABASE_STATE>(DATABASE_STATE, 'stickerSets');
private getStickerSetPromises: {[setId: Long]: Promise<MyMessagesStickerSet>}; private getStickerSetPromises: {[setId: Long]: Promise<MyMessagesStickerSet>};
private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<Document[]>}; private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<MyDocument[]>};
private greetingStickers: Document.document[]; private greetingStickers: MyDocument[];
private getGreetingStickersTimeout: number; private getGreetingStickersTimeout: number;
private getGreetingStickersPromise: Promise<void>; private getGreetingStickersPromise: Promise<void>;
@ -409,6 +409,14 @@ export class AppStickersManager extends AppManager {
return res.sets; return res.sets;
} }
public async getPromoPremiumStickers() {
return this.getStickersByEmoticon('⭐', false);
}
public async getPremiumStickers() {
return this.getStickersByEmoticon('📂⭐', false);
}
public async toggleStickerSet(set: StickerSet.stickerSet) { public async toggleStickerSet(set: StickerSet.stickerSet) {
set = this.storage.getFromCache(set.id).set; set = this.storage.getFromCache(set.id).set;

9
src/lib/crypto/crypto.worker.ts

@ -22,6 +22,7 @@ import rsaEncrypt from './utils/rsa';
import sha1 from './utils/sha1'; import sha1 from './utils/sha1';
import sha256 from './utils/sha256'; import sha256 from './utils/sha256';
import {aesCtrDestroy, aesCtrPrepare, aesCtrProcess} from './aesCtrUtils'; import {aesCtrDestroy, aesCtrPrepare, aesCtrProcess} from './aesCtrUtils';
import ctx from '../../environment/ctx';
console.log('CryptoWorker start'); console.log('CryptoWorker start');
@ -46,10 +47,16 @@ const cryptoMethods: CryptoMethods = {
'aes-ctr-destroy': aesCtrDestroy 'aes-ctr-destroy': aesCtrDestroy
}; };
cryptoMessagePort.addEventListener('invoke', ({method, args}) => { cryptoMessagePort.addMultipleEventsListeners({
invoke: ({method, args}) => {
// @ts-ignore // @ts-ignore
const result: any = cryptoMethods[method](...args); const result: any = cryptoMethods[method](...args);
return result; return result;
},
terminate: () => {
ctx.close();
}
}); });
if(typeof(MessageChannel) !== 'undefined') listenMessagePort(cryptoMessagePort, (source) => { if(typeof(MessageChannel) !== 'undefined') listenMessagePort(cryptoMessagePort, (source) => {

21
src/lib/crypto/cryptoMessagePort.ts

@ -12,15 +12,23 @@ import {IS_WORKER} from '../../helpers/context';
type CryptoEvent = { type CryptoEvent = {
invoke: <T extends keyof CryptoMethods>(payload: {method: T, args: Parameters<CryptoMethods[T]>}) => ReturnType<CryptoMethods[T]>, invoke: <T extends keyof CryptoMethods>(payload: {method: T, args: Parameters<CryptoMethods[T]>}) => ReturnType<CryptoMethods[T]>,
port: (payload: void, source: MessageEventSource, event: MessageEvent) => void port: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
terminate: () => void
}; };
export class CryptoMessagePort<Master extends boolean = false> extends SuperMessagePort<CryptoEvent, CryptoEvent, Master> { export class CryptoMessagePort<Master extends boolean = false> extends SuperMessagePort<CryptoEvent, CryptoEvent, Master> {
private lastIndex: number;
constructor() { constructor() {
super('CRYPTO'); super('CRYPTO');
this.lastIndex = -1;
} }
public invokeCrypto<T extends keyof CryptoMethods>(method: T, ...args: Parameters<CryptoMethods[T]>): Promise<Awaited<ReturnType<CryptoMethods[T]>>> { public invokeCryptoNew<T extends keyof CryptoMethods>({method, args, transfer}: {
method: T,
args: Parameters<CryptoMethods[T]>,
transfer?: Transferable[]
}): Promise<Awaited<ReturnType<CryptoMethods[T]>>> {
const payload = {method, args}; const payload = {method, args};
const listeners = this.listeners['invoke']; const listeners = this.listeners['invoke'];
if(listeners?.length) { // already in worker if(listeners?.length) { // already in worker
@ -37,8 +45,15 @@ export class CryptoMessagePort<Master extends boolean = false> extends SuperMess
// } // }
} }
const sendPortIndex = method === 'aes-encrypt' || method === 'aes-decrypt' ?
this.lastIndex = (this.lastIndex + 1) % this.sendPorts.length :
0;
// @ts-ignore // @ts-ignore
return this.invoke('invoke', payload); return this.invoke('invoke', payload, undefined, this.sendPorts[sendPortIndex], transfer);
}
public invokeCrypto<T extends keyof CryptoMethods>(method: T, ...args: Parameters<CryptoMethods[T]>): Promise<Awaited<ReturnType<CryptoMethods[T]>>> {
return this.invokeCryptoNew({method, args});
} }
} }

21
src/lib/mtproto/apiFileManager.ts

@ -29,7 +29,7 @@ import MTProtoMessagePort from './mtprotoMessagePort';
import getFileNameForUpload from '../../helpers/getFileNameForUpload'; import getFileNameForUpload from '../../helpers/getFileNameForUpload';
import type {Progress} from '../appManagers/appDownloadManager'; import type {Progress} from '../appManagers/appDownloadManager';
import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails'; import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails';
import networkStats from './networkStats'; // import networkStats from './networkStats';
import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions'; import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions';
import StreamWriter from '../files/streamWriter'; import StreamWriter from '../files/streamWriter';
import FileStorage from '../files/fileStorage'; import FileStorage from '../files/fileStorage';
@ -89,7 +89,8 @@ const MIN_PART_SIZE = 128 * 1024;
const AVG_PART_SIZE = 512 * 1024; const AVG_PART_SIZE = 512 * 1024;
const REGULAR_DOWNLOAD_DELTA = (9 * 512 * 1024) / MIN_PART_SIZE; const REGULAR_DOWNLOAD_DELTA = (9 * 512 * 1024) / MIN_PART_SIZE;
const PREMIUM_DOWNLOAD_DELTA = REGULAR_DOWNLOAD_DELTA * 2; // const PREMIUM_DOWNLOAD_DELTA = REGULAR_DOWNLOAD_DELTA * 2;
const PREMIUM_DOWNLOAD_DELTA = (56 * 512 * 1024) / MIN_PART_SIZE;
const IGNORE_ERRORS: Set<ErrorType> = new Set([ const IGNORE_ERRORS: Set<ErrorType> = new Set([
'DOWNLOAD_CANCELED', 'DOWNLOAD_CANCELED',
@ -201,15 +202,15 @@ export class ApiFileManager extends AppManager {
this.downloadActives[dcId] += activeDelta; this.downloadActives[dcId] += activeDelta;
const promise = data.cb(); const promise = data.cb();
const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * MIN_PART_SIZE); // const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * MIN_PART_SIZE);
Promise.race([ /* Promise.race([
promise, promise
networkPromise // networkPromise
]).then(() => { ]) */promise.then(() => {
this.downloadActives[dcId] -= activeDelta; this.downloadActives[dcId] -= activeDelta;
this.downloadCheck(dcId); this.downloadCheck(dcId);
networkPromise.resolve(); // networkPromise.resolve();
}, (error: ApiError) => { }, (error: ApiError) => {
if(!error?.type || !IGNORE_ERRORS.has(error.type)) { if(!error?.type || !IGNORE_ERRORS.has(error.type)) {
this.log.error('downloadCheck error:', error); this.log.error('downloadCheck error:', error);
@ -218,7 +219,7 @@ export class ApiFileManager extends AppManager {
this.downloadActives[dcId] -= activeDelta; this.downloadActives[dcId] -= activeDelta;
this.downloadCheck(dcId); this.downloadCheck(dcId);
networkPromise.reject(error); // networkPromise.reject(error);
}).finally(() => { }).finally(() => {
promise.then(data.deferred.resolve, data.deferred.reject); promise.then(data.deferred.resolve, data.deferred.reject);
}); });
@ -543,7 +544,7 @@ export class ApiFileManager extends AppManager {
} }
delete item.writer; delete item.writer;
indexOfAndSplice(prepared, item); // indexOfAndSplice(prepared, item);
}); });
this.downloadPromises[fileName] = deferred; this.downloadPromises[fileName] = deferred;

1
src/lib/mtproto/mtproto.worker.ts

@ -132,6 +132,7 @@ appTabsManager.start();
listenMessagePort(port, (source) => { listenMessagePort(port, (source) => {
appTabsManager.addTab(source); appTabsManager.addTab(source);
// port.invokeVoid('hello', undefined, source);
// if(!sentHello) { // if(!sentHello) {
// port.invokeVoid('hello', undefined, source); // port.invokeVoid('hello', undefined, source);
// sentHello = true; // sentHello = true;

1
src/lib/mtproto/mtprotoMessagePort.ts

@ -31,6 +31,7 @@ export default class MTProtoMessagePort<Master extends boolean = true> extends S
cryptoPort: (payload: void, source: MessageEventSource, event: MessageEvent) => void, cryptoPort: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
createObjectURL: (blob: Blob) => string, createObjectURL: (blob: Blob) => string,
tabState: (payload: TabState, source: MessageEventSource) => void, tabState: (payload: TabState, source: MessageEventSource) => void,
createProxyWorkerURLs: (blob: Blob) => string[],
} & MTProtoBroadcastEvent, { } & MTProtoBroadcastEvent, {
convertWebp: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>, convertWebp: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>,
convertOpus: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>, convertOpus: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>,

62
src/lib/mtproto/mtprotoworker.ts

@ -27,6 +27,7 @@ import IS_SHARED_WORKER_SUPPORTED from '../../environment/sharedWorkerSupport';
import toggleStorages from '../../helpers/toggleStorages'; import toggleStorages from '../../helpers/toggleStorages';
import idleController from '../../helpers/idleController'; import idleController from '../../helpers/idleController';
import ServiceMessagePort from '../serviceWorker/serviceMessagePort'; import ServiceMessagePort from '../serviceWorker/serviceMessagePort';
import App from '../../config/app';
export type Mirrors = { export type Mirrors = {
state: State state: State
@ -81,6 +82,7 @@ class ApiManagerProxy extends MTProtoMessagePort {
this.registerServiceWorker(); this.registerServiceWorker();
this.registerCryptoWorker(); this.registerCryptoWorker();
// const perf = performance.now();
this.addMultipleEventsListeners({ this.addMultipleEventsListeners({
convertWebp: ({fileName, bytes}) => { convertWebp: ({fileName, bytes}) => {
return webpWorkerController.convert(fileName, bytes); return webpWorkerController.convert(fileName, bytes);
@ -101,6 +103,10 @@ class ApiManagerProxy extends MTProtoMessagePort {
}, },
mirror: this.onMirrorTask mirror: this.onMirrorTask
// hello: () => {
// this.log.error('time hello', performance.now() - perf);
// }
}); });
// this.addTaskListener('socketProxy', (task) => { // this.addTaskListener('socketProxy', (task) => {
@ -276,27 +282,61 @@ class ApiManagerProxy extends MTProtoMessagePort {
}); });
} }
private registerCryptoWorker() { private async registerCryptoWorker() {
let worker: SharedWorker | Worker; const get = (url: string) => {
if(IS_SHARED_WORKER_SUPPORTED) { return fetch(url).then((response) => response.text()).then((text) => {
worker = new SharedWorker( text = 'var a = importScripts; importScripts = (url) => {console.log(`wut`, url); return a(url.slice(5));};' + text;
/* webpackChunkName: "crypto.worker" */ const blob = new Blob([text], {type: 'application/javascript'});
new URL('../crypto/crypto.worker.ts', import.meta.url), return blob;
{type: 'module'} });
); };
} else {
worker = new Worker( const workerHandler = {
construct(target: any, args: any): any {
const url = args[0] + location.search;
return {url};
}
};
const originals = [
Worker,
typeof(SharedWorker) !== 'undefined' && SharedWorker
].filter(Boolean);
originals.forEach((w) => window[w.name as any] = new Proxy(w, workerHandler));
const worker: SharedWorker | Worker = new Worker(
/* webpackChunkName: "crypto.worker" */ /* webpackChunkName: "crypto.worker" */
new URL('../crypto/crypto.worker.ts', import.meta.url), new URL('../crypto/crypto.worker.ts', import.meta.url),
{type: 'module'} {type: 'module'}
); );
}
originals.forEach((w) => window[w.name as any] = w as any);
const blob = await get((worker as any).url);
const urlsPromise = await this.invoke('createProxyWorkerURLs', blob);
const workers = urlsPromise.map((url) => {
return new (IS_SHARED_WORKER_SUPPORTED ? SharedWorker : Worker)(url, {type: 'module'});
});
// let cryptoWorkers = workers.length;
cryptoMessagePort.addEventListener('port', (payload, source, event) => { cryptoMessagePort.addEventListener('port', (payload, source, event) => {
this.invokeVoid('cryptoPort', undefined, undefined, [event.ports[0]]); this.invokeVoid('cryptoPort', undefined, undefined, [event.ports[0]]);
// .then((attached) => {
// if(!attached && cryptoWorkers-- > 1) {
// this.log.error('terminating unneeded crypto worker');
// cryptoMessagePort.invokeVoid('terminate', undefined, source);
// const worker = workers.find((worker) => (worker as SharedWorker).port === source || (worker as any) === source);
// if((worker as SharedWorker).port) (worker as SharedWorker).port.close();
// else (worker as Worker).terminate();
// cryptoMessagePort.detachPort(source);
// }
// });
}); });
workers.forEach((worker) => {
this.attachWorkerToPort(worker, cryptoMessagePort, 'crypto'); this.attachWorkerToPort(worker, cryptoMessagePort, 'crypto');
});
} }
// #if !MTPROTO_SW // #if !MTPROTO_SW

13
src/lib/mtproto/networker.ts

@ -112,6 +112,7 @@ const RESEND_OPTIONS: MTMessageOptions = {
notContentRelated: true notContentRelated: true
}; };
let invokeAfterMsgConstructor: number; let invokeAfterMsgConstructor: number;
let networkerTempId = 0;
export default class MTPNetworker { export default class MTPNetworker {
private authKeyUint8: Uint8Array; private authKeyUint8: Uint8Array;
@ -178,6 +179,7 @@ export default class MTPNetworker {
private pingPromise: Promise<void>; private pingPromise: Promise<void>;
// private pingInterval: number; // private pingInterval: number;
private lastPingTime: number; private lastPingTime: number;
private lastPingStartTime: number;
private lastPingDelayDisconnectId: string; private lastPingDelayDisconnectId: string;
// #endif // #endif
// public onConnectionStatusChange: (online: boolean) => void; // public onConnectionStatusChange: (online: boolean) => void;
@ -207,7 +209,7 @@ export default class MTPNetworker {
const suffix = this.isFileUpload ? '-U' : this.isFileDownload ? '-D' : ''; const suffix = this.isFileUpload ? '-U' : this.isFileDownload ? '-D' : '';
this.name = 'NET-' + dcId + suffix; this.name = 'NET-' + dcId + suffix;
// this.log = logger(this.name, this.upload && this.dcId === 2 ? LogLevels.debug | LogLevels.warn | LogLevels.log | LogLevels.error : LogLevels.error); // this.log = logger(this.name, this.upload && this.dcId === 2 ? LogLevels.debug | LogLevels.warn | LogLevels.log | LogLevels.error : LogLevels.error);
this.log = logger(this.name, LogTypes.Log /* | LogTypes.Debug */ | LogTypes.Error | LogTypes.Warn); this.log = logger(this.name + (suffix ? '' : '-C') + '-' + networkerTempId++, LogTypes.Log/* | LogTypes.Debug */ | LogTypes.Error | LogTypes.Warn);
this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */); this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */);
// Test resend after bad_server_salt // Test resend after bad_server_salt
@ -563,7 +565,7 @@ export default class MTPNetworker {
const lastPingTime = Math.min(this.lastPingTime ?? 0, pingMaxTime); const lastPingTime = Math.min(this.lastPingTime ?? 0, pingMaxTime);
const disconnectDelay = Math.round(delays.disconnectDelayMin + lastPingTime / pingMaxTime * (delays.disconnectDelayMax - delays.disconnectDelayMin)); const disconnectDelay = Math.round(delays.disconnectDelayMin + lastPingTime / pingMaxTime * (delays.disconnectDelayMax - delays.disconnectDelayMin));
const timeoutTime = disconnectDelay * 1000; const timeoutTime = disconnectDelay * 1000;
const startTime = Date.now(); const startTime = this.lastPingStartTime = Date.now();
const pingId = this.lastPingDelayDisconnectId = randomLong(); const pingId = this.lastPingDelayDisconnectId = randomLong();
const options: MTMessageOptions = {notContentRelated: true}; const options: MTMessageOptions = {notContentRelated: true};
this.wrapMtpCall('ping_delay_disconnect', { this.wrapMtpCall('ping_delay_disconnect', {
@ -571,14 +573,15 @@ export default class MTPNetworker {
disconnect_delay: disconnectDelay disconnect_delay: disconnectDelay
}, options); }, options);
this.debug && this.log.debug(`sendPingDelayDisconnect: ping, timeout=${timeoutTime}, lastPingTime=${this.lastPingTime}, msgId=${options.messageId}`); const log = this.log.bindPrefix('sendPingDelayDisconnect');
this.debug && log.debug(`ping, timeout=${timeoutTime}, lastPingTime=${this.lastPingTime}, msgId=${options.messageId}, pingId=${pingId}`);
const rejectTimeout = ctx.setTimeout(deferred.reject, timeoutTime); const rejectTimeout = ctx.setTimeout(deferred.reject, timeoutTime);
const onResolved = (reason: string) => { const onResolved = (reason: string) => {
clearTimeout(rejectTimeout); clearTimeout(rejectTimeout);
const elapsedTime = Date.now() - startTime; const elapsedTime = Date.now() - startTime;
this.lastPingTime = elapsedTime / 1000; this.lastPingTime = elapsedTime / 1000;
this.debug && this.log.debug(`sendPingDelayDisconnect: pong, reason='${reason}', time=${lastPingTime}, msgId=${options.messageId}`); this.debug && log.debug(`pong, reason='${reason}', time=${lastPingTime}, msgId=${options.messageId}`);
if(elapsedTime > timeoutTime) { if(elapsedTime > timeoutTime) {
throw undefined; throw undefined;
} else { } else {
@ -593,7 +596,7 @@ export default class MTPNetworker {
return; return;
} }
this.log.error('sendPingDelayDisconnect: catch, closing connection', this.lastPingTime, options.messageId); log.error('catch, closing connection', this.lastPingTime, options.messageId);
transport.connection.close(); transport.connection.close();
}; };

6
src/lib/mtproto/transports/http.ts

@ -13,7 +13,7 @@ import Modes from '../../../config/modes';
// #if MTPROTO_AUTO // #if MTPROTO_AUTO
import transportController from './controller'; import transportController from './controller';
import networkStats from '../networkStats'; // import networkStats from '../networkStats';
// #endif // #endif
export default class HTTP implements MTTransport { export default class HTTP implements MTTransport {
@ -47,7 +47,7 @@ export default class HTTP implements MTTransport {
const length = body.length; const length = body.length;
this.debug && this.log.debug('-> body length to send:', length); this.debug && this.log.debug('-> body length to send:', length);
networkStats.addSent(this.dcId, length); // networkStats.addSent(this.dcId, length);
return fetch(this.url, {method: 'POST', body, mode}).then((response) => { return fetch(this.url, {method: 'POST', body, mode}).then((response) => {
if(response.status !== 200 && !mode) { if(response.status !== 200 && !mode) {
response.arrayBuffer().then((buffer) => { response.arrayBuffer().then((buffer) => {
@ -66,7 +66,7 @@ export default class HTTP implements MTTransport {
// } // }
return response.arrayBuffer().then((buffer) => { return response.arrayBuffer().then((buffer) => {
networkStats.addReceived(this.dcId, buffer.byteLength); // networkStats.addReceived(this.dcId, buffer.byteLength);
return new Uint8Array(buffer); return new Uint8Array(buffer);
}); });
}, (err) => { }, (err) => {

7
src/lib/mtproto/transports/obfuscation.ts

@ -157,10 +157,11 @@ export default class Obfuscation {
} */ } */
private _process = (data: Uint8Array, operation: 'encrypt' | 'decrypt') => { private _process = (data: Uint8Array, operation: 'encrypt' | 'decrypt') => {
return cryptoMessagePort.invoke('invoke', { return cryptoMessagePort.invokeCryptoNew({
method: 'aes-ctr-process', method: 'aes-ctr-process',
args: [{id: this.id, data, operation}] args: [{id: this.id, data, operation}],
}, undefined, undefined, [data.buffer]) as Promise<Uint8Array>; transfer: [data.buffer]
}) as Promise<Uint8Array>;
}; };
public encode(payload: Uint8Array) { public encode(payload: Uint8Array) {

6
src/lib/mtproto/transports/tcpObfuscated.ts

@ -17,7 +17,7 @@ import {ConnectionStatus} from '../connectionStatus';
// #if MTPROTO_AUTO // #if MTPROTO_AUTO
import transportController from './controller'; import transportController from './controller';
import bytesToHex from '../../../helpers/bytes/bytesToHex'; import bytesToHex from '../../../helpers/bytes/bytesToHex';
import networkStats from '../networkStats'; // import networkStats from '../networkStats';
import ctx from '../../../environment/ctx'; import ctx from '../../../environment/ctx';
// #endif // #endif
@ -94,7 +94,7 @@ export default class TcpObfuscated implements MTTransport {
}; };
private onMessage = async(buffer: ArrayBuffer) => { private onMessage = async(buffer: ArrayBuffer) => {
networkStats.addReceived(this.dcId, buffer.byteLength); // networkStats.addReceived(this.dcId, buffer.byteLength);
let data = await this.obfuscation.decode(new Uint8Array(buffer)); let data = await this.obfuscation.decode(new Uint8Array(buffer));
data = this.codec.readPacket(data); data = this.codec.readPacket(data);
@ -343,7 +343,7 @@ export default class TcpObfuscated implements MTTransport {
break; break;
} }
networkStats.addSent(this.dcId, encoded.byteLength); // networkStats.addSent(this.dcId, encoded.byteLength);
this.connection.send(encoded); this.connection.send(encoded);
if(!pending.resolve) { // remove if no response needed if(!pending.resolve) { // remove if no response needed

8
src/lib/rlottie/lottieLoader.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import animationIntersector from '../../components/animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../../components/animationIntersector';
import {MOUNT_CLASS_TO} from '../../config/debug'; import {MOUNT_CLASS_TO} from '../../config/debug';
import pause from '../../helpers/schedulers/pause'; import pause from '../../helpers/schedulers/pause';
import {logger, LogTypes} from '../logger'; import {logger, LogTypes} from '../logger';
@ -131,7 +131,11 @@ export class LottieLoader {
]).then(() => player); ]).then(() => player);
} }
public async loadAnimationWorker(params: RLottieOptions, group = params.group || '', middleware?: () => boolean): Promise<RLottiePlayer> { public async loadAnimationWorker(
params: RLottieOptions,
group: AnimationItemGroup = params.group || '',
middleware?: () => boolean
): Promise<RLottiePlayer> {
if(!this.isWebAssemblySupported) { if(!this.isWebAssemblySupported) {
return this.loadPromise as any; return this.loadPromise as any;
} }

10
src/lib/rlottie/rlottiePlayer.ts

@ -12,6 +12,7 @@ import mediaSizes from '../../helpers/mediaSizes';
import clamp from '../../helpers/number/clamp'; import clamp from '../../helpers/number/clamp';
import lottieLoader from './lottieLoader'; import lottieLoader from './lottieLoader';
import QueryableWorker from './queryableWorker'; import QueryableWorker from './queryableWorker';
import {AnimationItemGroup} from '../../components/animationIntersector';
export type RLottieOptions = { export type RLottieOptions = {
container: HTMLElement, container: HTMLElement,
@ -21,7 +22,7 @@ export type RLottieOptions = {
loop?: boolean, loop?: boolean,
width?: number, width?: number,
height?: number, height?: number,
group?: string, group?: AnimationItemGroup,
noCache?: boolean, noCache?: boolean,
needUpscale?: boolean, needUpscale?: boolean,
skipRatio?: number, skipRatio?: number,
@ -86,7 +87,8 @@ export default class RLottiePlayer extends EventListenerBase<{
enterFrame: (frameNo: number) => void, enterFrame: (frameNo: number) => void,
ready: () => void, ready: () => void,
firstFrame: () => void, firstFrame: () => void,
cached: () => void cached: () => void,
destroy: () => void
}> { }> {
private static reqId = 0; private static reqId = 0;
@ -241,6 +243,7 @@ export default class RLottiePlayer extends EventListenerBase<{
this.canvas.classList.add('rlottie'); this.canvas.classList.add('rlottie');
this.canvas.width = this.width; this.canvas.width = this.width;
this.canvas.height = this.height; this.canvas.height = this.height;
this.canvas.dpr = pixelRatio;
} }
this.context = this.canvas.getContext('2d'); this.context = this.canvas.getContext('2d');
@ -354,8 +357,9 @@ export default class RLottiePlayer extends EventListenerBase<{
this.pause(); this.pause();
this.sendQuery('destroy'); this.sendQuery('destroy');
if(this.cacheName) cache.releaseCache(this.cacheName); if(this.cacheName) cache.releaseCache(this.cacheName);
this.cleanup(); this.dispatchEvent('destroy');
// this.removed = true; // this.removed = true;
this.cleanup();
} }
private applyColor(frame: Uint8ClampedArray) { private applyColor(frame: Uint8ClampedArray) {

8
src/scss/mixins/_premium.scss

@ -4,13 +4,13 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
@mixin premium($not: false) { @mixin premium($yes: true) {
@if $not { @if $yes {
body:not(.is-premium) & { body.is-premium & {
@content; @content;
} }
} @else { } @else {
body.is-premium & { body:not(.is-premium) & {
@content; @content;
} }
} }

40
src/scss/style.scss

@ -126,6 +126,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--peer-avatar-pink-top: #e0a2f3; --peer-avatar-pink-top: #e0a2f3;
--peer-avatar-pink-bottom: #d669ed; --peer-avatar-pink-bottom: #d669ed;
--premium-gradient: linear-gradient(52.62deg, #6B93FF 12.22%, #976FFF 50.25%, #E46ACE 98.83%);
@include respond-to(handhelds) { @include respond-to(handhelds) {
--right-column-width: 100vw; --right-column-width: 100vw;
--esg-sticker-size: 68px; --esg-sticker-size: 68px;
@ -592,6 +594,12 @@ input:-webkit-autofill:active {
} }
} }
.color-premium {
background: var(--premium-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.blue:before, .primary:before, .danger:before { .blue:before, .primary:before, .danger:before {
color: inherit !important; color: inherit !important;
} }
@ -1338,6 +1346,38 @@ middle-ellipsis-element {
height: 100%; height: 100%;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
&-wrapper {
&.is-premium-sticker {
&:before {
// content: " ";
position: absolute;
bottom: .125rem;
left: 50%;
transform: translateX(-50%);
width: 1.25rem;
height: 1.25rem;
background: rgba(0, 0, 0, .2);
// backdrop-filter: blur(25px) saturate(1.5);
border-radius: 50%;
color: #fff;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
background-image: var(--lock-url);
background-repeat: no-repeat;
background-size: cover;
}
@include premium(true) {
&:before,
&:after {
content: none;
}
}
}
}
} }
.media-round { .media-round {

Loading…
Cancel
Save