From 628fbfec268ef0b0b95fdef779bb1fe27194021f Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Wed, 1 Mar 2023 14:20:49 +0400 Subject: [PATCH] 1 --- src/components/animationIntersector.ts | 16 +- src/components/appMediaViewerBase.ts | 3 +- src/components/appSelectPeers.ts | 3 +- src/components/chat/autocompleteHelper.ts | 3 +- src/components/chat/bubbles.ts | 9 +- src/components/chat/chat.ts | 3 +- src/components/chat/gradientRenderer.ts | 2 +- src/components/chat/reaction.ts | 5 + src/components/chat/reactions.ts | 2 +- src/components/chat/reactionsMenu.ts | 6 +- src/components/chat/sendAs.ts | 3 +- src/components/checkboxFields.ts | 157 +++++++++++++ src/components/dotRenderer.ts | 12 +- .../emoticonsDropdown/tabs/emoji.ts | 5 +- src/components/horizontalMenu.ts | 3 +- src/components/poll.ts | 3 +- src/components/popups/limit.ts | 3 +- src/components/popups/newMedia.ts | 3 +- src/components/putPhoto.ts | 3 +- src/components/ripple.ts | 5 +- src/components/row.ts | 9 +- .../sidebarLeft/tabs/dataAndStorage.ts | 24 -- .../sidebarLeft/tabs/generalSettings.ts | 29 ++- .../sidebarLeft/tabs/powerSaving.ts | 131 +++++++++++ src/components/sidebarLeft/tabs/settings.ts | 1 - .../sidebarRight/tabs/groupPermissions.ts | 208 +++++------------- .../sidebarRight/tabs/sharedMedia.ts | 3 +- src/components/singleTransition.ts | 5 +- src/components/transition.ts | 3 +- src/components/wrappers/document.ts | 3 +- src/components/wrappers/photo.ts | 3 +- src/components/wrappers/sticker.ts | 47 +++- src/components/wrappers/stickerAnimation.ts | 3 +- src/components/wrappers/stickerSetThumb.ts | 5 +- src/components/wrappers/video.ts | 8 +- src/config/app.ts | 2 +- src/config/state.ts | 46 ++-- src/helpers/dialogsPlaceholder.ts | 7 +- src/helpers/dom/shake.ts | 3 +- src/helpers/dom/sortable.ts | 3 +- src/helpers/dropdownHover.ts | 3 +- src/helpers/fastSmoothScroll.ts | 3 +- src/helpers/liteMode.ts | 24 ++ src/lang.ts | 25 +++ src/lib/appManagers/appImManager.ts | 44 +++- src/lib/richTextProcessor/wrapRichText.ts | 18 +- src/lib/rlottie/lottieLoader.ts | 22 +- src/lib/rlottie/rlottieIcon.ts | 3 +- src/lib/rlottie/rlottiePlayer.ts | 18 +- src/scss/partials/_checkbox.scss | 24 +- src/scss/partials/_leftSidebar.scss | 6 + 51 files changed, 701 insertions(+), 281 deletions(-) create mode 100644 src/components/checkboxFields.ts create mode 100644 src/components/sidebarLeft/tabs/powerSaving.ts create mode 100644 src/helpers/liteMode.ts diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index 2ca7426f..fe462e88 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type {LiteModeKey} from '../helpers/liteMode'; import {CustomEmojiElement, CustomEmojiRendererElement} from '../lib/richTextProcessor/wrapRichText'; import rootScope from '../lib/rootScope'; import {IS_SAFARI} from '../environment/userAgent'; @@ -25,6 +26,7 @@ export interface AnimationItem { el: HTMLElement, group: AnimationItemGroup, animation: AnimationItemWrapper, + liteModeKey?: LiteModeKey, controlled?: boolean | Middleware }; @@ -34,6 +36,7 @@ export interface AnimationItemWrapper { pause: () => any; play: () => any; autoplay: boolean; + loop: boolean | number; // onVisibilityChange?: (visible: boolean) => boolean; }; @@ -176,12 +179,14 @@ export class AnimationIntersector { } } - public addAnimation( + public addAnimation(options: { animation: AnimationItem['animation'], - group: AnimationItemGroup = '', + group?: AnimationItemGroup, observeElement?: HTMLElement, - controlled?: AnimationItem['controlled'] - ) { + controlled?: AnimationItem['controlled'], + liteModeKey?: LiteModeKey + }) { + let {animation, group = '', observeElement, controlled, liteModeKey} = options; if(group === 'none' || this.byPlayer.has(animation)) { return; } @@ -202,7 +207,8 @@ export class AnimationIntersector { el: observeElement, animation: animation, group, - controlled + controlled, + liteModeKey }; if(controlled && typeof(controlled) !== 'boolean') { diff --git a/src/components/appMediaViewerBase.ts b/src/components/appMediaViewerBase.ts index a694a13b..b97144f8 100644 --- a/src/components/appMediaViewerBase.ts +++ b/src/components/appMediaViewerBase.ts @@ -58,6 +58,7 @@ import clamp from '../helpers/number/clamp'; import debounce from '../helpers/schedulers/debounce'; import isBetween from '../helpers/number/isBetween'; import findUpAsChild from '../helpers/dom/findUpAsChild'; +import liteMode from '../helpers/liteMode'; const ZOOM_STEP = 0.5; const ZOOM_INITIAL_VALUE = 1; @@ -866,7 +867,7 @@ export default class AppMediaViewerBase< const wasActive = fromRight !== 0; - const delay = rootScope.settings.animationsEnabled ? (wasActive ? 350 : 200) : 0; + const delay = liteMode.isAvailable('animations') ? (wasActive ? 350 : 200) : 0; // let delay = wasActive ? 350 : 10000; /* if(wasActive) { diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 3d347aef..41602388 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -37,6 +37,7 @@ import hasRights from '../lib/appManagers/utils/chats/hasRights'; import getDialogIndex from '../lib/appManagers/utils/dialogs/getDialogIndex'; import {generateDelimiter} from './generateDelimiter'; import SettingSection from './settingSection'; +import liteMode from '../helpers/liteMode'; type SelectSearchPeerType = 'contacts' | 'dialogs' | 'channelParticipants'; @@ -678,7 +679,7 @@ export default class AppSelectPeers { this.onChange && this.onChange(this.selected.size); }; - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { div.addEventListener('animationend', onAnimationEnd, {once: true}); } else { onAnimationEnd(); diff --git a/src/components/chat/autocompleteHelper.ts b/src/components/chat/autocompleteHelper.ts index 63873ad9..8626ad2b 100644 --- a/src/components/chat/autocompleteHelper.ts +++ b/src/components/chat/autocompleteHelper.ts @@ -12,6 +12,7 @@ import appNavigationController, {NavigationItem} from '../appNavigationControlle import SetTransition from '../singleTransition'; import AutocompleteHelperController from './autocompleteHelperController'; import safeAssign from '../../helpers/object/safeAssign'; +import liteMode from '../../helpers/liteMode'; export default class AutocompleteHelper extends EventListenerBase<{ hidden: () => void, @@ -158,7 +159,7 @@ export default class AutocompleteHelper extends EventListenerBase<{ element: this.container, className: 'is-visible', forwards: !hide, - duration: rootScope.settings.animationsEnabled && !skipAnimation ? 300 : 0, + duration: liteMode.isAvailable('animations') && !skipAnimation ? 300 : 0, onTransitionEnd: () => { this.hidden && this.dispatchEvent('hidden'); }, diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index cb517f69..fc710697 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -132,6 +132,7 @@ import wrapPeerTitle from '../wrappers/peerTitle'; import {PopupPeerCheckboxOptions} from '../popups/peer'; import toggleDisability from '../../helpers/dom/toggleDisability'; import {copyTextToClipboard} from '../../helpers/clipboard'; +import liteMode from '../../helpers/liteMode'; export const USER_REACTIONS_INLINE = false; const USE_MEDIA_TAILS = false; @@ -1043,7 +1044,7 @@ export default class ChatBubbles { this.listenerSetter.add(rootScope)('history_append', async({storageKey, message}) => { if(storageKey !== this.chat.messagesStorageKey || this.chat.type === 'scheduled') return; - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('chat_background')) { this.updateGradient = true; } @@ -4458,6 +4459,7 @@ export default class ChatBubbles { group: this.chat.animationGroup, // play: !!message.pending || !multipleRender, play: true, + liteModeKey: 'stickers_chat', loop: true, emoji: isEmoji ? messageMessage : undefined, withThumb: true, @@ -5529,7 +5531,8 @@ export default class ChatBubbles { play: true, loop: true, withThumb: true, - loadPromises + loadPromises, + liteModeKey: 'stickers_chat' }); attachClickEvent(stickerDiv, (e) => { @@ -6227,7 +6230,7 @@ export default class ChatBubbles { const waitPromise = isAdditionRender ? processPromise(resultPromise) : promise; - if(isFirstMessageRender && rootScope.settings.animationsEnabled/* && false */) { + if(isFirstMessageRender && liteMode.isAvailable('animations')/* && false */) { let times = isAdditionRender ? 2 : 1; this.messagesQueueOnRenderAdditional = () => { this.log('messagesQueueOnRenderAdditional'); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index f0356856..b6325544 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -37,6 +37,7 @@ import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import {Message, WallPaper} from '../../layer'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector'; import {getColorsFromWallPaper} from '../../helpers/color'; +import liteMode from '../../helpers/liteMode'; export type ChatType = 'chat' | 'pinned' | 'discussion' | 'scheduled'; @@ -211,7 +212,7 @@ export default class Chat extends EventListenerBase<{ gradientCanvas = this.gradientCanvas = canvas; gradientCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-color-canvas'); - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { gradientRenderer.scrollAnimate(true); } // } else { diff --git a/src/components/chat/gradientRenderer.ts b/src/components/chat/gradientRenderer.ts index 1d0321f9..0759b85a 100644 --- a/src/components/chat/gradientRenderer.ts +++ b/src/components/chat/gradientRenderer.ts @@ -307,7 +307,7 @@ export default class ChatBackgroundGradientRenderer { this.update(); } - public update() { + private update() { if(this._colors.length < 2) { const color = this._colors[0]; this._ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`; diff --git a/src/components/chat/reaction.ts b/src/components/chat/reaction.ts index 5a839802..9b02f2cb 100644 --- a/src/components/chat/reaction.ts +++ b/src/components/chat/reaction.ts @@ -21,6 +21,7 @@ import RLottiePlayer from '../../lib/rlottie/rlottiePlayer'; import {fastRaf} from '../../helpers/schedulers'; import noop from '../../helpers/noop'; import {Middleware} from '../../helpers/middleware'; +import liteMode from '../../helpers/liteMode'; const CLASS_NAME = 'reaction'; const TAG_NAME = CLASS_NAME + '-element'; @@ -186,6 +187,10 @@ export default class ReactionElement extends HTMLElement { } public fireAroundAnimation() { + if(!liteMode.isAvailable('effects_reactions')) { + return; + } + const reaction = this.reactionCount.reaction; if(reaction._ !== 'reactionEmoji') return; callbackify(this.managers.appReactionsManager.getReaction(reaction.emoticon), (availableReaction) => { diff --git a/src/components/chat/reactions.ts b/src/components/chat/reactions.ts index 6ed81010..345a7c49 100644 --- a/src/components/chat/reactions.ts +++ b/src/components/chat/reactions.ts @@ -145,7 +145,7 @@ export default class ReactionsElement extends HTMLElement { const totalReactions = counts.reduce((acc, c) => acc + c.count, 0); const canRenderAvatars = reactions && (!!reactions.pFlags.can_see_list || this.message.peerId.isUser()) && totalReactions < REACTION_DISPLAY_BLOCK_COUNTER_AT; this.sorted = counts.map((reactionCount, idx) => { - let reactionElement = this.sorted.find((reactionElement) => reactionsEqual(reactionElement.reactionCount.reaction, reactionCount.reaction)); + let reactionElement: ReactionElement = this.sorted.find((reactionElement) => reactionsEqual(reactionElement.reactionCount.reaction, reactionCount.reaction)); if(!reactionElement) { const middlewareHelper = this.middleware.create(); reactionElement = new ReactionElement(); diff --git a/src/components/chat/reactionsMenu.ts b/src/components/chat/reactionsMenu.ts index 83bb6d7f..5f09926b 100644 --- a/src/components/chat/reactionsMenu.ts +++ b/src/components/chat/reactionsMenu.ts @@ -11,6 +11,7 @@ import callbackify from '../../helpers/callbackify'; import {attachClickEvent} from '../../helpers/dom/clickEvent'; import findUpClassName from '../../helpers/dom/findUpClassName'; import getVisibleRect from '../../helpers/dom/getVisibleRect'; +import liteMode from '../../helpers/liteMode'; import {getMiddleware} from '../../helpers/middleware'; import noop from '../../helpers/noop'; import {fastRaf} from '../../helpers/schedulers'; @@ -136,7 +137,7 @@ export class ChatReactionsMenu { }; private canUseAnimations() { - return rootScope.settings.animationsEnabled && !IS_MOBILE; + return liteMode.isAvailable('animations') && liteMode.isAvailable('stickers_chat') && !IS_MOBILE; } private renderReaction(reaction: AvailableReaction) { @@ -184,6 +185,7 @@ export class ChatReactionsMenu { wrapSticker({ doc: reaction.static_icon, div: appearWrapper, + liteModeKey: false, ...options }); } else { @@ -192,6 +194,7 @@ export class ChatReactionsMenu { doc: reaction.appear_animation, div: appearWrapper, play: true, + liteModeKey: false, ...options }).then(({render}) => render).then((player) => { assumeType(player); @@ -217,6 +220,7 @@ export class ChatReactionsMenu { const selectLoadPromise = wrapSticker({ doc: reaction.select_animation, div: selectWrapper, + liteModeKey: false, ...options }).then(({render}) => render).then((player) => { assumeType(player); diff --git a/src/components/chat/sendAs.ts b/src/components/chat/sendAs.ts index 1edd929b..0529f854 100644 --- a/src/components/chat/sendAs.ts +++ b/src/components/chat/sendAs.ts @@ -7,6 +7,7 @@ import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import callbackify from '../../helpers/callbackify'; import ListenerSetter from '../../helpers/listenerSetter'; +import liteMode from '../../helpers/liteMode'; import {getMiddleware} from '../../helpers/middleware'; import {modifyAckedPromise} from '../../helpers/modifyAckedResult'; import {ChatFull} from '../../layer'; @@ -148,7 +149,7 @@ export default class ChatSendAs { this.updateButtons(peerIds); }; - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { setTimeout(executeButtonsUpdate, 250); } else { executeButtonsUpdate(); diff --git a/src/components/checkboxFields.ts b/src/components/checkboxFields.ts new file mode 100644 index 00000000..92084105 --- /dev/null +++ b/src/components/checkboxFields.ts @@ -0,0 +1,157 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import cancelEvent from '../helpers/dom/cancelEvent'; +import {attachClickEvent} from '../helpers/dom/clickEvent'; +import findUpAsChild from '../helpers/dom/findUpAsChild'; +import ListenerSetter from '../helpers/listenerSetter'; +import safeAssign from '../helpers/object/safeAssign'; +import I18n, {LangPackKey} from '../lib/langPack'; +import CheckboxField from './checkboxField'; +import Row from './row'; +import {toast} from './toast'; + +export type CheckboxFieldsField = { + text: LangPackKey, + description?: LangPackKey, + restrictionText?: LangPackKey, + checkboxField?: CheckboxField, + checked?: boolean, + nested?: CheckboxFieldsField[], + nestedTo?: CheckboxFieldsField, + nestedCounter?: HTMLElement, + setNestedCounter?: (count: number) => void, + toggleWith?: {checked?: CheckboxFieldsField[], unchecked?: CheckboxFieldsField[]}, + name?: string, + row?: Row +}; + +export default class CheckboxFields { + public fields: Array; + protected listenerSetter: ListenerSetter; + protected asRestrictions: boolean; + + constructor(options: { + fields: Array, + listenerSetter: ListenerSetter, + asRestrictions?: boolean + }) { + safeAssign(this, options); + } + + public createField(info: CheckboxFieldsField, isNested?: boolean) { + if(info.nestedTo && !isNested) { + return; + } + + const row = info.row = new Row({ + titleLangKey: isNested ? undefined : info.text, + checkboxField: info.checkboxField = new CheckboxField({ + text: isNested ? info.text : undefined, + checked: info.nested ? false : info.checked, + toggle: !isNested, + listenerSetter: this.listenerSetter, + restriction: this.asRestrictions && !isNested, + name: info.name + }), + listenerSetter: this.listenerSetter, + subtitleLangKey: info.description, + clickable: info.nested ? (e) => { + if(findUpAsChild(e.target as HTMLElement, row.checkboxField.label)) { + return; + } + + cancelEvent(e); + row.container.classList.toggle('accordion-toggler-expanded'); + accordion.classList.toggle('is-expanded'); + } : undefined + }); + + if(info.restrictionText) { + info.checkboxField.input.disabled = true; + + attachClickEvent(info.checkboxField.label, (e) => { + toast(I18n.format(info.restrictionText, true)); + }, {listenerSetter: this.listenerSetter}); + } + + const nodes: HTMLElement[] = [row.container]; + let accordion: HTMLElement, nestedCounter: HTMLElement; + if(info.nested) { + const container = accordion = document.createElement('div'); + container.classList.add('accordion'); + container.style.setProperty('--max-height', info.nested.length * 48 + 'px'); + const _info = info; + info.nested.forEach((info) => { + info.nestedTo ??= _info; + container.append(...this.createField(info, true).nodes); + }); + nodes.push(container); + + const span = document.createElement('span'); + span.classList.add('tgico-down', 'accordion-icon'); + + nestedCounter = info.nestedCounter = document.createElement('b'); + this.setNestedCounter(info); + row.title.append(' ', nestedCounter, ' ', span); + + row.container.classList.add('accordion-toggler'); + row.titleRow.classList.add('with-delimiter'); + + row.checkboxField.setValueSilently(this.getNestedCheckedLength(info) === info.nested.length); + + info.toggleWith ??= {checked: info.nested, unchecked: info.nested}; + } + + if(info.toggleWith || info.nestedTo) { + const processToggleWith = info.toggleWith ? (info: CheckboxFieldsField) => { + const {toggleWith, nested} = info; + const value = info.checkboxField.checked; + const arr = value ? toggleWith.checked : toggleWith.unchecked; + if(!arr) { + return; + } + + const other = this.fields.filter((i) => arr.includes(i)); + other.forEach((info) => { + info.checkboxField.setValueSilently(value); + if(info.nestedTo && !nested) { + this.setNestedCounter(info.nestedTo); + } + + if(info.toggleWith) { + processToggleWith(info); + } + }); + + if(info.nested) { + this.setNestedCounter(info); + } + } : undefined; + + const processNestedTo = info.nestedTo ? () => { + const length = this.getNestedCheckedLength(info.nestedTo); + info.nestedTo.checkboxField.setValueSilently(length === info.nestedTo.nested.length); + this.setNestedCounter(info.nestedTo, length); + } : undefined; + + this.listenerSetter.add(info.checkboxField.input)('change', () => { + processToggleWith?.(info); + processNestedTo?.(); + }); + } + + return {row, nodes}; + } + + protected getNestedCheckedLength(info: CheckboxFieldsField) { + return info.nested.reduce((acc, v) => acc + +v.checkboxField.checked, 0); + } + + public setNestedCounter(info: CheckboxFieldsField, count = this.getNestedCheckedLength(info)) { + info.nestedCounter.textContent = `${count}/${info.nested.length}`; + } +} diff --git a/src/components/dotRenderer.ts b/src/components/dotRenderer.ts index 0c12d37e..b832c5c8 100644 --- a/src/components/dotRenderer.ts +++ b/src/components/dotRenderer.ts @@ -6,6 +6,7 @@ import {IS_MOBILE} from '../environment/userAgent'; import {animate} from '../helpers/animation'; +import liteMode from '../helpers/liteMode'; import {Middleware} from '../helpers/middleware'; import clamp from '../helpers/number/clamp'; import animationIntersector, {AnimationItemGroup, AnimationItemWrapper} from './animationIntersector'; @@ -31,6 +32,8 @@ export default class DotRenderer implements AnimationItemWrapper { private dpr: number; + public loop: boolean = true; + constructor( private width: number, private height: number, @@ -51,7 +54,7 @@ export default class DotRenderer implements AnimationItemWrapper { private prepare() { let count = Math.round(this.width * this.height / (35 * (IS_MOBILE ? 2 : 1))); count *= this.multiply || 1; - count = Math.min(IS_MOBILE ? 1000 : 2200, count); + count = Math.min(!liteMode.isAvailable('chat_spoilers') ? 400 : IS_MOBILE ? 1000 : 2200, count); count = Math.round(count); const dots: DotRendererDot[] = this.dots = new Array(count); @@ -168,7 +171,12 @@ export default class DotRenderer implements AnimationItemWrapper { const dotRenderer = new DotRenderer(width, height, multiply); dotRenderer.renderFirstFrame(); - animationIntersector.addAnimation(dotRenderer, animationGroup, dotRenderer.canvas, middleware); + animationIntersector.addAnimation({ + animation: dotRenderer, + group: animationGroup, + observeElement: dotRenderer.canvas, + controlled: middleware + }); return dotRenderer; } diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index 4d22dcad..3d3b2c25 100644 --- a/src/components/emoticonsDropdown/tabs/emoji.ts +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -39,6 +39,7 @@ import PopupStickers from '../../popups/stickers'; import {hideToast, toastNew} from '../../toast'; import safeAssign from '../../../helpers/object/safeAssign'; import type {AppStickersManager} from '../../../lib/appManagers/appStickersManager'; +import liteMode from '../../../helpers/liteMode'; const loadedURLs: Set = new Set(); export function appendEmoji(emoji: string, container?: HTMLElement, prepend = false, unify = false) { @@ -81,14 +82,14 @@ export function appendEmoji(emoji: string, container?: HTMLElement, prepend = fa const placeholder = document.createElement('span'); placeholder.classList.add('emoji-placeholder'); - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { image.style.opacity = '0'; placeholder.style.opacity = '1'; } image.addEventListener('load', () => { fastRaf(() => { - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { image.style.opacity = ''; placeholder.style.opacity = ''; } diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index bb143002..267d8357 100644 --- a/src/components/horizontalMenu.ts +++ b/src/components/horizontalMenu.ts @@ -13,6 +13,7 @@ import findUpAsChild from '../helpers/dom/findUpAsChild'; import whichChild from '../helpers/dom/whichChild'; import ListenerSetter from '../helpers/listenerSetter'; import {attachClickEvent} from '../helpers/dom/clickEvent'; +import liteMode from '../helpers/liteMode'; export function horizontalMenu( tabs: HTMLElement, @@ -66,7 +67,7 @@ export function horizontalMenu( }); } - if(!rootScope.settings.animationsEnabled) { + if(!liteMode.isAvailable('animations')) { animate = false; } diff --git a/src/components/poll.ts b/src/components/poll.ts index 22bcef3c..9b5843dd 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -26,6 +26,7 @@ import setInnerHTML from '../helpers/dom/setInnerHTML'; import {AppManagers} from '../lib/appManagers/managers'; import wrapEmojiText from '../lib/richTextProcessor/wrapEmojiText'; import wrapRichText from '../lib/richTextProcessor/wrapRichText'; +import liteMode from '../helpers/liteMode'; let lineTotalLength = 0; const tailLength = 9; @@ -528,7 +529,7 @@ export default class PollElement extends HTMLElement { } performResults(results: PollResults, chosenIndexes: number[], animate = true) { - if(!rootScope.settings.animationsEnabled) { + if(!liteMode.isAvailable('animations')) { animate = false; } diff --git a/src/components/popups/limit.ts b/src/components/popups/limit.ts index 6ca7ffee..931e9f6f 100644 --- a/src/components/popups/limit.ts +++ b/src/components/popups/limit.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import liteMode from '../../helpers/liteMode'; import {doubleRaf} from '../../helpers/schedulers'; import appImManager from '../../lib/appManagers/appImManager'; import {LangPackKey, _i18n, i18n} from '../../lib/langPack'; @@ -114,7 +115,7 @@ class P extends PopupPeer { hint.classList.add('active'); }; - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { doubleRaf().then(setHintActive); } else { setHintActive(); diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 8b3511b6..b3f5b6f6 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -47,6 +47,7 @@ import VIDEO_MIME_TYPES_SUPPORTED from '../../environment/videoMimeTypesSupport' import rootScope from '../../lib/rootScope'; import shake from '../../helpers/dom/shake'; import AUDIO_MIME_TYPES_SUPPORTED from '../../environment/audioMimeTypeSupport'; +import liteMode from '../../helpers/liteMode'; type SendFileParams = SendFileDetails & { file?: File, @@ -559,7 +560,7 @@ export default class PopupNewMedia extends PopupElement { langPackKey: key }); - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { shake(this.body); } } diff --git a/src/components/putPhoto.ts b/src/components/putPhoto.ts index cc5a5123..9eb37519 100644 --- a/src/components/putPhoto.ts +++ b/src/components/putPhoto.ts @@ -21,6 +21,7 @@ import getPeerInitials from './wrappers/getPeerInitials'; import {wrapTopicIcon} from './wrappers/messageActionTextNewUnsafe'; import makeError from '../helpers/makeError'; import noop from '../helpers/noop'; +import liteMode from '../helpers/liteMode'; export async function putAvatar( div: HTMLElement, @@ -46,7 +47,7 @@ export async function putAvatar( div.dataset.color = ''; }; } else { - const animate = rootScope.settings.animationsEnabled; + const animate = liteMode.isAvailable('animations'); if(animate) { img.classList.add('fade-in'); } diff --git a/src/components/ripple.ts b/src/components/ripple.ts index 733ebef7..dfdd75e7 100644 --- a/src/components/ripple.ts +++ b/src/components/ripple.ts @@ -10,6 +10,7 @@ import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; import rootScope from '../lib/rootScope'; import findUpAsChild from '../helpers/dom/findUpAsChild'; import {fastRaf} from '../helpers/schedulers'; +import liteMode from '../helpers/liteMode'; let rippleClickId = 0; export default function ripple( @@ -167,7 +168,7 @@ export default function ripple( }; attachListenerTo.addEventListener('touchstart', (e) => { - if(!rootScope.settings.animationsEnabled) { + if(!liteMode.isAvailable('animations')) { return; } @@ -196,7 +197,7 @@ export default function ripple( return; } - if(!rootScope.settings.animationsEnabled) { + if(!liteMode.isAvailable('animations')) { return; } // console.log('ripple mousedown', e, e.target, findUpClassName(e.target as HTMLElement, 'c-ripple') === r); diff --git a/src/components/row.ts b/src/components/row.ts index c96d5bdf..4027eee8 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -285,10 +285,15 @@ export default class Row { return media; } + public toggleDisability(disable = !this.container.classList.contains('is-disabled')) { + this.container.classList.toggle('is-disabled', disable); + return () => this.toggleDisability(!disable); + } + public disableWithPromise(promise: Promise) { - this.container.classList.add('is-disabled'); + const toggle = this.toggleDisability(true); promise.finally(() => { - this.container.classList.remove('is-disabled'); + toggle(); }); } diff --git a/src/components/sidebarLeft/tabs/dataAndStorage.ts b/src/components/sidebarLeft/tabs/dataAndStorage.ts index 2cfd0ed5..1c5817a3 100644 --- a/src/components/sidebarLeft/tabs/dataAndStorage.ts +++ b/src/components/sidebarLeft/tabs/dataAndStorage.ts @@ -154,30 +154,6 @@ export default class AppDataAndStorageTab extends SliderSuperTabEventable { this.scrollable.append(section.container); } - - { - const section = new SettingSection({name: 'AutoplayMedia'}); - - section.content.append(new Row({ - checkboxField: new CheckboxField({ - text: 'AutoplayGIF', - name: 'gifs', - stateKey: 'settings.autoPlay.gifs', - listenerSetter: this.listenerSetter - }), - listenerSetter: this.listenerSetter - }).container, new Row({ - checkboxField: new CheckboxField({ - text: 'AutoplayVideo', - name: 'videos', - stateKey: 'settings.autoPlay.videos', - listenerSetter: this.listenerSetter - }), - listenerSetter: this.listenerSetter - }).container); - - this.scrollable.append(section.container); - } } private setAutoDownloadSubtitle(row: Row, settings: AutoDownloadPeerTypeSettings, sizeMax?: number) { diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index cd8b4e21..65d68969 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -12,7 +12,7 @@ import rootScope from '../../../lib/rootScope'; import {IS_APPLE, IS_SAFARI} from '../../../environment/userAgent'; import Row, {CreateRowFromCheckboxField} from '../../row'; import AppBackgroundTab from './background'; -import {LangPackKey, _i18n} from '../../../lib/langPack'; +import I18n, {i18n, LangPackKey, _i18n} from '../../../lib/langPack'; import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import assumeType from '../../../helpers/assumeType'; import {BaseTheme, MessagesAllStickers, StickerSet} from '../../../layer'; @@ -33,6 +33,8 @@ import {Theme} from '../../../layer'; import findUpClassName from '../../../helpers/dom/findUpClassName'; import RLottiePlayer from '../../../lib/rlottie/rlottiePlayer'; import themeController from '../../../helpers/themeController'; +import liteMode from '../../../helpers/liteMode'; +import AppPowerSavingTab from './powerSaving'; export class RangeSettingSelector { public container: HTMLDivElement; @@ -123,17 +125,30 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { this.slider.createTab(AppBackgroundTab).open(initArgs); }); - const animationsCheckboxField = new CheckboxField({ - text: 'EnableAnimations', - name: 'animations', - stateKey: 'settings.animationsEnabled', + const getLiteModeStatus = (): LangPackKey => rootScope.settings.liteMode.all ? 'Checkbox.Enabled' : 'Checkbox.Disabled'; + const i = new I18n.IntlElement(); + + const onUpdate = () => { + i.compareAndUpdate({key: getLiteModeStatus()}); + }; + onUpdate(); + + const liteModeRow = new Row({ + icon: 'animations', + titleLangKey: 'LiteMode.EnableText', + titleRightSecondary: i.element, + clickable: () => { + this.slider.createTab(AppPowerSavingTab).open(); + }, listenerSetter: this.listenerSetter }); + this.listenerSetter.add(rootScope)('settings_updated', onUpdate); + container.append( range.container, chatBackgroundButton, - CreateRowFromCheckboxField(animationsCheckboxField).container + liteModeRow.container ); } @@ -186,7 +201,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable { lastOnFrameNo?.(-1); - if(item.player && rootScope.settings.animationsEnabled) { + if(item.player && liteMode.isAvailable('animations')) { if(IS_SAFARI) { if(item.player.paused) { item.player.restart(); diff --git a/src/components/sidebarLeft/tabs/powerSaving.ts b/src/components/sidebarLeft/tabs/powerSaving.ts new file mode 100644 index 00000000..219e58ad --- /dev/null +++ b/src/components/sidebarLeft/tabs/powerSaving.ts @@ -0,0 +1,131 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import {State} from '../../../config/state'; +import flatten from '../../../helpers/array/flatten'; +import {attachClickEvent} from '../../../helpers/dom/clickEvent'; +import {LiteModeKey} from '../../../helpers/liteMode'; +import pause from '../../../helpers/schedulers/pause'; +import rootScope from '../../../lib/rootScope'; +import CheckboxFields, {CheckboxFieldsField} from '../../checkboxFields'; +import SettingSection from '../../settingSection'; +import SliderSuperTab from '../../sliderTab'; +import {toastNew} from '../../toast'; + +type PowerSavingCheckboxFieldsField = CheckboxFieldsField & { + key: LiteModeKey +}; + +export default class AppPowerSavingTab extends SliderSuperTab { + public init() { + this.container.classList.add('power-saving-container'); + this.setTitle('LiteMode.Title'); + + const form = document.createElement('form'); + + let infoSection: SettingSection; + { + const section = infoSection = new SettingSection({ + caption: 'LiteMode.Info' + }); + + form.append(section.container); + } + + const keys: Array = [ + 'all', + 'video', + 'gif', + ['stickers', ['stickers_panel', 'stickers_chat']], + ['emoji', ['emoji_panel', 'emoji_messages']], + ['effects', ['effects_reactions', 'effects_premiumstickers', 'effects_emoji']], + ['chat', ['chat_background', 'chat_spoilers']], + 'animations' + ]; + + let fields: PowerSavingCheckboxFieldsField[], checkboxFields: CheckboxFields; + { + const section = new SettingSection({}); + + const wrap = (key: typeof keys[0]): PowerSavingCheckboxFieldsField[] => { + const isArray = Array.isArray(key); + const mainKey = isArray ? key[0] : key; + const nested = isArray ? flatten(key[1].map(wrap)) : undefined; + const value = rootScope.settings.liteMode[mainKey]; + return [{ + key: mainKey, + text: mainKey === 'all' ? 'LiteMode.EnableText' : `LiteMode.Key.${mainKey}.Title`, + checked: mainKey === 'all' ? value : !value, + nested: nested, + name: 'power-saving-' + mainKey + }, ...(nested || [])]; + }; + + fields = flatten(keys.map(wrap)); + + checkboxFields = new CheckboxFields({ + fields: fields, + listenerSetter: this.listenerSetter + }); + + fields.forEach((field, idx) => { + const created = checkboxFields.createField(field); + if(!created) { + return; + } + + const {row, nodes} = created; + (idx === 0 ? infoSection : section).content.append(...nodes); + }); + + attachClickEvent(section.content, () => { + if(rootScope.settings.liteMode.all) { + toastNew({langPackKey: 'LiteMode.DisableAlert'}); + } + }, {listenerSetter: this.listenerSetter}); + + form.append(section.container); + } + + const onAllChange = (disable: boolean) => { + fields.forEach((field) => { + if(field.key === 'all') { + return; + } + + if(field.nested) { + checkboxFields.setNestedCounter(field, disable ? 0 : undefined); + } + + field.checkboxField.input.classList.toggle('is-fake-disabled', disable); + field.row.toggleDisability(disable); + }); + }; + + this.listenerSetter.add(form)('change', async() => { + const liteMode: State['settings']['liteMode'] = {} as any; + fields.forEach((field) => { + const checked = field.checkboxField.checked; + liteMode[field.key] = field.key === 'all' ? checked : !checked; + }); + + const wasAll = rootScope.settings.liteMode.all; + if(wasAll !== liteMode.all) { + onAllChange(!wasAll); + + if(liteMode.all) { + await pause(200); + } + } + + await this.managers.appStateManager.setByKey('settings.liteMode', rootScope.settings.liteMode = liteMode); + }); + + onAllChange(rootScope.settings.liteMode.all); + + this.scrollable.append(form); + } +} diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index e043243a..e4e2ee9c 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -28,7 +28,6 @@ import {AccountAuthorizations, Authorization} from '../../../layer'; import PopupElement from '../../popups'; import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import SettingSection from '../../settingSection'; -// import AppMediaViewer from "../../appMediaViewerNew"; export default class AppSettingsTab extends SliderSuperTab { private buttons: { diff --git a/src/components/sidebarRight/tabs/groupPermissions.ts b/src/components/sidebarRight/tabs/groupPermissions.ts index a484e480..2ebd0d54 100644 --- a/src/components/sidebarRight/tabs/groupPermissions.ts +++ b/src/components/sidebarRight/tabs/groupPermissions.ts @@ -5,8 +5,6 @@ */ import type {ChatRights} from '../../../lib/appManagers/appChatsManager'; -import flatten from '../../../helpers/array/flatten'; -import cancelEvent from '../../../helpers/dom/cancelEvent'; import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import findUpTag from '../../../helpers/dom/findUpTag'; import replaceContent from '../../../helpers/dom/replaceContent'; @@ -19,32 +17,22 @@ import combineParticipantBannedRights from '../../../lib/appManagers/utils/chats import hasRights from '../../../lib/appManagers/utils/chats/hasRights'; import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames'; import getPeerId from '../../../lib/appManagers/utils/peers/getPeerId'; -import I18n, {i18n, join, LangPackKey} from '../../../lib/langPack'; +import {i18n, join, LangPackKey} from '../../../lib/langPack'; import rootScope from '../../../lib/rootScope'; -import CheckboxField from '../../checkboxField'; import PopupPickUser from '../../popups/pickUser'; import Row from '../../row'; import SettingSection from '../../settingSection'; import {SliderSuperTabEventable} from '../../sliderTab'; import {toast} from '../../toast'; import AppUserPermissionsTab from './userPermissions'; -import findUpAsChild from '../../../helpers/dom/findUpAsChild'; +import CheckboxFields, {CheckboxFieldsField} from '../../checkboxFields'; -type T = { +type PermissionsCheckboxFieldsField = CheckboxFieldsField & { flags: ChatRights[], - text: LangPackKey, - exceptionText: LangPackKey, - checkboxField?: CheckboxField, - nested?: T[], - nestedTo?: T, - nestedCounter?: HTMLElement, - setNestedCounter?: (count: number) => void, - toggleWith?: {checked?: ChatRights[], unchecked?: ChatRights[]} + exceptionText: LangPackKey }; -export class ChatPermissions { - public v: Array; - +export class ChatPermissions extends CheckboxFields { protected chat: Chat.chat | Chat.channel; protected rights: ChatBannedRights.chatBannedRights; protected defaultBannedRights: ChatBannedRights.chatBannedRights; @@ -56,11 +44,22 @@ export class ChatPermissions { appendTo: HTMLElement, participant?: ChannelParticipant.channelParticipantBanned }, private managers: AppManagers) { + super({ + listenerSetter: options.listenerSetter, + fields: [], + asRestrictions: true + }); + this.construct(); } public async construct() { - const mediaNested: T[] = [ + const options = this.options; + const chat = this.chat = await this.managers.appChatsManager.getChat(options.chatId) as Chat.chat | Chat.channel; + const defaultBannedRights = this.defaultBannedRights = chat.default_banned_rights; + const rights = this.rights = options.participant ? combineParticipantBannedRights(chat as Chat.channel, options.participant.banned_rights) : defaultBannedRights; + + const mediaNested: PermissionsCheckboxFieldsField[] = [ {flags: ['send_photos'], text: 'UserRestrictionsSendPhotos', exceptionText: 'UserRestrictionsNoSendPhotos'}, {flags: ['send_videos'], text: 'UserRestrictionsSendVideos', exceptionText: 'UserRestrictionsNoSendVideos'}, {flags: ['send_stickers', 'send_gifs'], text: 'UserRestrictionsSendStickers', exceptionText: 'UserRestrictionsNoSendStickers'}, @@ -68,150 +67,57 @@ export class ChatPermissions { {flags: ['send_docs'], text: 'UserRestrictionsSendFiles', exceptionText: 'UserRestrictionsNoSendDocs'}, {flags: ['send_voices'], text: 'UserRestrictionsSendVoices', exceptionText: 'UserRestrictionsNoSendVoice'}, {flags: ['send_roundvideos'], text: 'UserRestrictionsSendRound', exceptionText: 'UserRestrictionsNoSendRound'}, - {flags: ['embed_links'], text: 'UserRestrictionsEmbedLinks', exceptionText: 'UserRestrictionsNoEmbedLinks', toggleWith: {checked: ['send_plain']}}, + {flags: ['embed_links'], text: 'UserRestrictionsEmbedLinks', exceptionText: 'UserRestrictionsNoEmbedLinks'}, {flags: ['send_polls'], text: 'UserRestrictionsSendPolls', exceptionText: 'UserRestrictionsNoSendPolls'} ]; - const mediaToggleWith = flatten(mediaNested.map(({flags}) => flags)); - const media: T = {flags: ['send_media'], text: 'UserRestrictionsSendMedia', exceptionText: 'UserRestrictionsNoSendMedia', nested: mediaNested, toggleWith: {unchecked: mediaToggleWith, checked: mediaToggleWith}}; - - this.v = [ - {flags: ['send_plain'], text: 'UserRestrictionsSend', exceptionText: 'UserRestrictionsNoSend', toggleWith: {unchecked: ['embed_links']}}, - media, + const mediaToggleWith = mediaNested; + const v: PermissionsCheckboxFieldsField[] = [ + {flags: ['send_plain'], text: 'UserRestrictionsSend', exceptionText: 'UserRestrictionsNoSend'}, + {flags: ['send_media'], text: 'UserRestrictionsSendMedia', exceptionText: 'UserRestrictionsNoSendMedia', nested: mediaNested}, {flags: ['invite_users'], text: 'UserRestrictionsInviteUsers', exceptionText: 'UserRestrictionsNoInviteUsers'}, {flags: ['pin_messages'], text: 'UserRestrictionsPinMessages', exceptionText: 'UserRestrictionsNoPinMessages'}, {flags: ['change_info'], text: 'UserRestrictionsChangeInfo', exceptionText: 'UserRestrictionsNoChangeInfo'} ]; - mediaNested.forEach((info) => info.nestedTo = media); - const options = this.options; - const chat = this.chat = await this.managers.appChatsManager.getChat(options.chatId) as Chat.chat | Chat.channel; - const defaultBannedRights = this.defaultBannedRights = chat.default_banned_rights; - const rights = this.rights = options.participant ? combineParticipantBannedRights(chat as Chat.channel, options.participant.banned_rights) : defaultBannedRights; - - for(const info of this.v) { - const {nodes} = this.createRow(info); - options.appendTo.append(...nodes); - } - - this.v.push(...mediaNested); - } - - protected createRow(info: T, isNested?: boolean) { - const {defaultBannedRights, chat, rights, restrictionText} = this; - - const mainFlag = info.flags[0]; - const row = new Row({ - titleLangKey: isNested ? undefined : info.text, - checkboxField: info.checkboxField = new CheckboxField({ - text: isNested ? info.text : undefined, - checked: info.nested ? false : hasRights(chat, mainFlag, rights), - toggle: !isNested, - listenerSetter: this.options.listenerSetter, - restriction: !isNested - }), - listenerSetter: this.options.listenerSetter, - clickable: info.nested ? (e) => { - if(findUpAsChild(e.target as HTMLElement, row.checkboxField.label)) { - return; - } - - cancelEvent(e); - row.container.classList.toggle('accordion-toggler-expanded'); - accordion.classList.toggle('is-expanded'); - } : undefined + const map: {[action in ChatRights]?: PermissionsCheckboxFieldsField} = {}; + v.push(...mediaNested); + v.forEach((info) => { + const mainFlag = info.flags[0]; + map[mainFlag] = info; + info.checked = hasRights(chat, mainFlag, rights) }); - if(( - this.options.participant && - defaultBannedRights.pFlags[mainFlag as keyof typeof defaultBannedRights['pFlags']] - ) || ( - getPeerActiveUsernames(chat as Chat.channel)[0] && - ( - info.flags.includes('pin_messages') || - info.flags.includes('change_info') - ) - ) - ) { - info.checkboxField.input.disabled = true; + mediaNested.forEach((info) => info.nestedTo = map.send_media); + map.send_media.toggleWith = {unchecked: mediaToggleWith, checked: mediaToggleWith}; + map.embed_links.toggleWith = {checked: [map.send_plain]}; + map.send_plain.toggleWith = {unchecked: [map.embed_links]}; - attachClickEvent(info.checkboxField.label, (e) => { - toast(I18n.format(restrictionText, true)); - }, {listenerSetter: this.options.listenerSetter}); + this.fields = v; + + for(const info of this.fields) { + if(( + this.options.participant && + defaultBannedRights.pFlags[info.flags[0] as keyof typeof defaultBannedRights['pFlags']] + ) || ( + getPeerActiveUsernames(chat as Chat.channel)[0] && + ( + info.flags.includes('pin_messages') || + info.flags.includes('change_info') + ) + ) + ) { + info.restrictionText = this.restrictionText; + } + + if(info.nestedTo) { + continue; + } + + const {nodes} = this.createField(info); + options.appendTo.append(...nodes); } - - if(info.toggleWith || info.nestedTo) { - const processToggleWith = info.toggleWith ? (info: T) => { - const {toggleWith, nested} = info; - const value = info.checkboxField.checked; - const arr = value ? toggleWith.checked : toggleWith.unchecked; - if(!arr) { - return; - } - - const other = this.v.filter((i) => arr.includes(i.flags[0])); - other.forEach((info) => { - info.checkboxField.setValueSilently(value); - if(info.nestedTo && !nested) { - this.setNestedCounter(info.nestedTo); - } - - if(info.toggleWith) { - processToggleWith(info); - } - }); - - if(info.nested) { - this.setNestedCounter(info); - } - } : undefined; - - const processNestedTo = info.nestedTo ? () => { - const length = this.getNestedCheckedLength(info.nestedTo); - info.nestedTo.checkboxField.setValueSilently(length === info.nestedTo.nested.length); - this.setNestedCounter(info.nestedTo, length); - } : undefined; - - this.options.listenerSetter.add(info.checkboxField.input)('change', () => { - processToggleWith?.(info); - processNestedTo?.(); - }); - } - - const nodes: HTMLElement[] = [row.container]; - let accordion: HTMLElement, nestedCounter: HTMLElement; - if(info.nested) { - const container = accordion = document.createElement('div'); - container.classList.add('accordion'); - container.style.setProperty('--max-height', info.nested.length * 48 + 'px'); - info.nested.forEach((info) => { - container.append(...this.createRow(info, true).nodes); - }); - nodes.push(container); - - const span = document.createElement('span'); - span.classList.add('tgico-down', 'accordion-icon'); - - nestedCounter = info.nestedCounter = document.createElement('b'); - this.setNestedCounter(info); - row.title.append(' ', nestedCounter, ' ', span); - - row.container.classList.add('accordion-toggler'); - row.titleRow.classList.add('with-delimiter'); - - row.checkboxField.setValueSilently(this.getNestedCheckedLength(info) === info.nested.length); - } - - return {row, nodes}; - } - - protected getNestedCheckedLength(info: T) { - return info.nested.reduce((acc, v) => acc + +v.checkboxField.checked, 0); - } - - protected setNestedCounter(info: T, count = this.getNestedCheckedLength(info)) { - info.nestedCounter.textContent = `${count}/${info.nested.length}`; } public takeOut() { @@ -224,7 +130,7 @@ export class ChatPermissions { const IGNORE_FLAGS: Set = new Set([ 'send_media' ]); - for(const info of this.v) { + for(const info of this.fields) { const banned = !info.checkboxField.checked; if(!banned) { continue; @@ -341,7 +247,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable { // const combinedRights = appChatsManager.combineParticipantBannedRights(this.chatId, bannedRights); const cantWhat: LangPackKey[] = []/* , canWhat: LangPackKey[] = [] */; - chatPermissions.v.forEach((info) => { + chatPermissions.fields.forEach((info) => { const mainFlag = info.flags[0]; // @ts-ignore if(bannedRights.pFlags[mainFlag] && !defaultBannedRights.pFlags[mainFlag]) { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 617af586..bc81cc85 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -24,6 +24,7 @@ import PeerProfile from '../../peerProfile'; import {Message} from '../../../layer'; import getMessageThreadId from '../../../lib/appManagers/utils/messages/getMessageThreadId'; import AppEditTopicTab from './editTopic'; +import liteMode from '../../../helpers/liteMode'; type SharedMediaHistoryStorage = Partial<{ [type in SearchSuperType]: {mid: number, peerId: PeerId}[] @@ -226,7 +227,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { }], scrollable: this.scrollable, onChangeTab: (mediaTab) => { - const timeout = mediaTab.type === 'members' && rootScope.settings.animationsEnabled ? 250 : 0; + const timeout = mediaTab.type === 'members' && liteMode.isAvailable('animations') ? 250 : 0; setTimeout(() => { btnAddMembers.classList.toggle('is-hidden', mediaTab.type !== 'members'); }, timeout); diff --git a/src/components/singleTransition.ts b/src/components/singleTransition.ts index 8be00859..21a52c4b 100644 --- a/src/components/singleTransition.ts +++ b/src/components/singleTransition.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import liteMode from '../helpers/liteMode'; import rootScope from '../lib/rootScope'; type SetTransitionOptions = { @@ -36,7 +37,7 @@ const SetTransition = (options: SetTransitionOptions) => { // return; // } - if(useRafs && rootScope.settings.animationsEnabled && duration) { + if(useRafs && liteMode.isAvailable('animations') && duration) { element.dataset.raf = '' + window.requestAnimationFrame(() => { delete element.dataset.raf; SetTransition({ @@ -64,7 +65,7 @@ const SetTransition = (options: SetTransitionOptions) => { }; onTransitionStart?.(); - if(!rootScope.settings.animationsEnabled || !duration) { + if(!liteMode.isAvailable('animations') || !duration) { element.classList.remove('animating', 'backwards'); afterTimeout(); return; diff --git a/src/components/transition.ts b/src/components/transition.ts index 42aa111a..c95aa995 100644 --- a/src/components/transition.ts +++ b/src/components/transition.ts @@ -10,6 +10,7 @@ import {dispatchHeavyAnimationEvent} from '../hooks/useHeavyAnimationCheck'; import whichChild from '../helpers/dom/whichChild'; import cancelEvent from '../helpers/dom/cancelEvent'; import ListenerSetter from '../helpers/listenerSetter'; +import liteMode from '../helpers/liteMode'; function makeTransitionFunction(options: TransitionFunction) { return options; @@ -216,7 +217,7 @@ const TransitionSlider = (options: TransitionSliderOptions) => { const to = content.children[id] as HTMLElement; - if(!rootScope.settings.animationsEnabled || (prevId === -1 && !animateFirst)) { + if(!liteMode.isAvailable('animations') || (prevId === -1 && !animateFirst)) { animate = false; } diff --git a/src/components/wrappers/document.ts b/src/components/wrappers/document.ts index 5c52ccb1..bbb6a83b 100644 --- a/src/components/wrappers/document.ts +++ b/src/components/wrappers/document.ts @@ -11,6 +11,7 @@ import {formatFullSentTime} from '../../helpers/date'; import {simulateClickEvent, attachClickEvent} from '../../helpers/dom/clickEvent'; import findUpClassName from '../../helpers/dom/findUpClassName'; import formatBytes from '../../helpers/formatBytes'; +import liteMode from '../../helpers/liteMode'; import {MediaSizeType} from '../../helpers/mediaSizes'; import noop from '../../helpers/noop'; import {Message, MessageMedia, WebPage} from '../../layer'; @@ -289,7 +290,7 @@ export default async function wrapDocument({message, withTime, fontWeight, voice setTimeout(async() => { // wait for preloader animation end const url = (await getCacheContext()).url; window.open(url); - }, rootScope.settings.animationsEnabled ? 250 : 0); + }, liteMode.isAvailable('animations') ? 250 : 0); }); } } else if(MEDIA_MIME_TYPES_SUPPORTED.has(doc.mime_type) && doc.thumbs?.length) { diff --git a/src/components/wrappers/photo.ts b/src/components/wrappers/photo.ts index 6492a0ad..de66e878 100644 --- a/src/components/wrappers/photo.ts +++ b/src/components/wrappers/photo.ts @@ -24,6 +24,7 @@ import createVideo from '../../helpers/dom/createVideo'; import noop from '../../helpers/noop'; import {THUMB_TYPE_FULL} from '../../lib/mtproto/mtproto_config'; import {Middleware} from '../../helpers/middleware'; +import liteMode from '../../helpers/liteMode'; export default async function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, autoDownloadSize, noBlur, noThumb, noFadeIn, blurAfter, managers = rootScope.managers, processUrl}: { photo: MyPhoto | MyDocument | WebDocument, @@ -191,7 +192,7 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo // console.log('wrapPhoto downloaded:', photo, photo.downloaded, container); - const needFadeIn = (thumbImage || !cacheContext.downloaded) && rootScope.settings.animationsEnabled && !noFadeIn; + const needFadeIn = (thumbImage || !cacheContext.downloaded) && liteMode.isAvailable('animations') && !noFadeIn; let preloader: ProgressivePreloader; const uploadingFileName = (message as Message.message)?.uploadingFileName; diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index a561e7ef..788c0ec9 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -45,6 +45,7 @@ import PopupStickers from '../popups/stickers'; import {hideToast, toastNew} from '../toast'; import wrapStickerAnimation from './stickerAnimation'; import framesCache from '../../helpers/framesCache'; +import liteMode, {LiteModeKey} from '../../helpers/liteMode'; // https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/history/view/media/history_view_sticker.cpp#L40 export const STICKER_EFFECT_MULTIPLIER = 1 + 0.245 * 2; @@ -64,7 +65,7 @@ const onAnimationEnd = (element: HTMLElement, onAnimationEnd: () => void, timeou const _timeout = setTimeout(onEnd, timeout); }; -export default async function wrapSticker({doc, div, middleware, loadStickerMiddleware, lazyLoadQueue, exportLoad, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio, static: asStatic, managers = rootScope.managers, fullThumb, isOut, noPremium, withLock, relativeEffect, loopEffect, isCustomEmoji, syncedVideo}: { +export default async function wrapSticker({doc, div, middleware, loadStickerMiddleware, lazyLoadQueue, exportLoad, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn, needUpscale, skipRatio, static: asStatic, managers = rootScope.managers, fullThumb, isOut, noPremium, withLock, relativeEffect, loopEffect, isCustomEmoji, syncedVideo, liteModeKey, isEffect}: { doc: MyDocument, div: HTMLElement | HTMLElement[], middleware?: Middleware, @@ -92,10 +93,14 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd relativeEffect?: boolean, loopEffect?: boolean, isCustomEmoji?: boolean, - syncedVideo?: boolean + syncedVideo?: boolean, + liteModeKey?: LiteModeKey | false, + isEffect?: boolean }) { div = Array.isArray(div) ? div : [div]; + liteModeKey ??= 'stickers_panel'; + if(isCustomEmoji) { emoji = doc.stickerEmojiRaw; } @@ -118,15 +123,25 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd lottieLoader.loadLottieWorkers(); } + loop = !!(!emoji || isCustomEmoji) && loop; + div.forEach((div) => { div.dataset.docId = '' + doc.id; if(emoji) { div.dataset.stickerEmoji = emoji; } + div.dataset.stickerPlay = '' + +play; + div.dataset.stickerLoop = '' + +loop; + div.classList.add('media-sticker-wrapper'); }); + if(play && !liteMode.isAvailable(liteModeKey) && !isCustomEmoji && !isEffect) { + play = false; + loop = false; + } + /* if(stickerType === 3) { const videoRes = wrapVideo({ doc, @@ -277,7 +292,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd const path = document.createElementNS(ns, 'path'); path.setAttributeNS(null, 'd', d); - if(rootScope.settings.animationsEnabled && !isCustomEmoji) path.setAttributeNS(null, 'fill', 'url(#g)'); + if(liteMode.isAvailable('animations') && !isCustomEmoji) path.setAttributeNS(null, 'fill', 'url(#g)'); svg.append(path); div.forEach((div, idx) => div.append(idx > 0 ? svg.cloneNode(true) : svg)); haveThumbCached = true; @@ -383,7 +398,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd const animation = await lottieLoader.loadAnimationWorker({ container: (div as HTMLElement[])[0], - loop: !!(!emoji || isCustomEmoji) && loop, + loop, autoplay: play, animationData: blob, width, @@ -394,7 +409,8 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd toneIndex, sync: isCustomEmoji, middleware: loadStickerMiddleware ?? middleware, - group + group, + liteModeKey: liteModeKey || undefined }); // const deferred = deferredPromise(); @@ -407,7 +423,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd const onFirstFrame = (container: HTMLElement, canvas: HTMLCanvasElement) => { const element = container.firstElementChild !== canvas && container.firstElementChild as HTMLElement; if(needFadeIn !== false) { - needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled; + needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && liteMode.isAvailable('animations'); } const cb = () => { @@ -505,7 +521,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd const thumbImage = (div as HTMLElement[]).map((div, idx) => (div.firstElementChild as HTMLElement) !== media[idx] && div.firstElementChild) as HTMLElement[]; if(needFadeIn !== false) { - needFadeIn = (needFadeIn || !downloaded || (asStatic ? thumbImage[0] : (!thumbImage[0] || thumbImage[0].tagName === 'svg'))) && rootScope.settings.animationsEnabled; + needFadeIn = (needFadeIn || !downloaded || (asStatic ? thumbImage[0] : (!thumbImage[0] || thumbImage[0].tagName === 'svg'))) && liteMode.isAvailable('animations'); } if(needFadeIn) { @@ -569,7 +585,13 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd } if(isAnimated) { - animationIntersector.addAnimation(media as HTMLVideoElement, group, undefined, middleware); + animationIntersector.addAnimation({ + animation: media as HTMLVideoElement, + observeElement: div, + group, + controlled: middleware, + liteModeKey: liteModeKey || undefined + }); } if(loaded.push(media) === mediaLength) { @@ -666,8 +688,13 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut let playing = false; attachClickEvent(container, async(e) => { + const isAvailable = liteMode.isAvailable('effects_premiumstickers') || relativeEffect; cancelEvent(e); - if(playing) { + if(!e.isTrusted && !isAvailable) { + return; + } + + if(playing || !isAvailable) { const a = document.createElement('a'); a.onclick = () => { hideToast(); @@ -749,7 +776,7 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m animation.restart(); } - if(!peerId.isUser()) { + if(!peerId.isUser() || !liteMode.isAvailable('effects_emoji')) { return; } diff --git a/src/components/wrappers/stickerAnimation.ts b/src/components/wrappers/stickerAnimation.ts index 6b727297..2f436cb3 100644 --- a/src/components/wrappers/stickerAnimation.ts +++ b/src/components/wrappers/stickerAnimation.ts @@ -76,7 +76,8 @@ export default function wrapStickerAnimation({ group: 'none', skipRatio, managers, - fullThumb + fullThumb, + isEffect: true }).then(({render}) => render).then((_animation) => { assumeType(_animation); if(!middleware()) { diff --git a/src/components/wrappers/stickerSetThumb.ts b/src/components/wrappers/stickerSetThumb.ts index 86b635f0..214ff2ce 100644 --- a/src/components/wrappers/stickerSetThumb.ts +++ b/src/components/wrappers/stickerSetThumb.ts @@ -69,7 +69,10 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container container.append(media); if(set.pFlags.videos) { - animationIntersector.addAnimation(media as HTMLVideoElement, group); + animationIntersector.addAnimation({ + animation: media as HTMLVideoElement, + group + }); } }); }); diff --git a/src/components/wrappers/video.ts b/src/components/wrappers/video.ts index 6a92131f..07bf9b1a 100644 --- a/src/components/wrappers/video.ts +++ b/src/components/wrappers/video.ts @@ -14,6 +14,7 @@ import createVideo from '../../helpers/dom/createVideo'; import isInDOM from '../../helpers/dom/isInDOM'; import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl'; import getStrippedThumbIfNeeded from '../../helpers/getStrippedThumbIfNeeded'; +import liteMode from '../../helpers/liteMode'; import makeError from '../../helpers/makeError'; import mediaSizes, {ScreenSize} from '../../helpers/mediaSizes'; import {Middleware} from '../../helpers/middleware'; @@ -96,7 +97,7 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem ) - ) && (doc.type === 'gif' ? rootScope.settings.autoPlay.gifs : rootScope.settings.autoPlay.videos) + ) && (doc.type === 'gif' ? liteMode.isAvailable('gif') : liteMode.isAvailable('video')) ); let spanTime: HTMLElement, spanPlay: HTMLElement; @@ -537,7 +538,10 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH onMediaLoad(video).then(() => { if(group) { - animationIntersector.addAnimation(video, group); + animationIntersector.addAnimation({ + animation: video, + group + }); } if(preloader && !uploadFileName) { diff --git a/src/config/app.ts b/src/config/app.ts index 8cfbe9ae..de12f523 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -21,7 +21,7 @@ const App = { version: process.env.VERSION, versionFull: process.env.VERSION_FULL, build: +process.env.BUILD, - langPackVersion: '0.9.3', + langPackVersion: '0.9.7', langPack: 'webk', langPackCode: 'en', domains: MAIN_DOMAINS, diff --git a/src/config/state.ts b/src/config/state.ts index 7a462bab..727cde94 100644 --- a/src/config/state.ts +++ b/src/config/state.ts @@ -4,15 +4,16 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import {AppMediaPlaybackController} from '../components/appMediaPlaybackController'; +import type {LiteModeKey} from '../helpers/liteMode'; +import type {AppMediaPlaybackController} from '../components/appMediaPlaybackController'; +import type {TopPeerType, MyTopPeer} from '../lib/appManagers/appUsersManager'; +import type {AutoDownloadSettings, BaseTheme, NotifyPeer, PeerNotifySettings, Theme, ThemeSettings, WallPaper} from '../layer'; +import type DialogsStorage from '../lib/storages/dialogs'; +import type FiltersStorage from '../lib/storages/filters'; +import type {AuthState, Modify} from '../types'; import {IS_MOBILE} from '../environment/userAgent'; import getTimeFormat from '../helpers/getTimeFormat'; import {nextRandomUint} from '../helpers/random'; -import {AutoDownloadSettings, BaseTheme, NotifyPeer, PeerNotifySettings, Theme, ThemeSettings, WallPaper} from '../layer'; -import {TopPeerType, MyTopPeer} from '../lib/appManagers/appUsersManager'; -import DialogsStorage from '../lib/storages/dialogs'; -import FiltersStorage from '../lib/storages/filters'; -import {AuthState, Modify} from '../types'; import App from './app'; const STATE_VERSION = App.version; @@ -74,7 +75,7 @@ export type State = { messagesTextSize: number, distanceUnit: 'kilometers' | 'miles', sendShortcut: 'enter' | 'ctrlEnter', - animationsEnabled: boolean, + animationsEnabled?: boolean, // ! DEPRECATED autoDownload: { contacts?: boolean, // ! DEPRECATED private?: boolean, // ! DEPRECATED @@ -85,7 +86,7 @@ export type State = { file: AutoDownloadPeerTypeSettings }, autoDownloadNew: AutoDownloadSettings, - autoPlay: { + autoPlay?: { // ! DEPRECATED gifs: boolean, videos: boolean }, @@ -104,7 +105,8 @@ export type State = { sound: boolean }, nightTheme?: boolean, // ! DEPRECATED - timeFormat: 'h12' | 'h23' + timeFormat: 'h12' | 'h23', + liteMode: {[key in LiteModeKey]: boolean} }, playbackParams: ReturnType, keepSigned: boolean, @@ -233,7 +235,6 @@ export const STATE_INIT: State = { messagesTextSize: 16, distanceUnit: 'kilometers', sendShortcut: 'enter', - animationsEnabled: true, autoDownload: { photo: { contacts: true, @@ -265,10 +266,6 @@ export const STATE_INIT: State = { video_size_max: 15728640, video_upload_maxbitrate: 100 }, - autoPlay: { - gifs: true, - videos: true - }, stickers: { suggest: true, loop: true @@ -285,7 +282,26 @@ export const STATE_INIT: State = { notifications: { sound: false }, - timeFormat: getTimeFormat() + timeFormat: getTimeFormat(), + liteMode: { + all: false, + animations: false, + chat: false, + chat_background: false, + chat_spoilers: false, + effects: false, + effects_premiumstickers: false, + effects_reactions: false, + effects_emoji: false, + emoji: false, + emoji_messages: false, + emoji_panel: false, + gif: false, + stickers: false, + stickers_chat: false, + stickers_panel: false, + video: false + } }, playbackParams: { volume: 1, diff --git a/src/helpers/dialogsPlaceholder.ts b/src/helpers/dialogsPlaceholder.ts index 0463932b..a6536f8c 100644 --- a/src/helpers/dialogsPlaceholder.ts +++ b/src/helpers/dialogsPlaceholder.ts @@ -12,6 +12,7 @@ import roundRect from './canvas/roundRect'; import Shimmer from './canvas/shimmer'; import customProperties from './dom/customProperties'; import easeInOutSine from './easing/easeInOutSine'; +import liteMode from './liteMode'; import mediaSizes from './mediaSizes'; export default class DialogsPlaceholder { @@ -91,7 +92,7 @@ export default class DialogsPlaceholder { this.availableLength = availableLength; this.detachTime = Date.now(); - if(!rootScope.settings.animationsEnabled) { + if(!liteMode.isAvailable('animations')) { this.remove(); } } @@ -132,7 +133,7 @@ export default class DialogsPlaceholder { if(!detachTime) { return; - } else if(!rootScope.settings.animationsEnabled) { + } else if(!liteMode.isAvailable('animations')) { this.remove(); return; } @@ -219,7 +220,7 @@ export default class DialogsPlaceholder { } // ! should've removed the loop if animations are disabled - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { this.renderFrame(); } diff --git a/src/helpers/dom/shake.ts b/src/helpers/dom/shake.ts index fdd929a5..680e90b3 100644 --- a/src/helpers/dom/shake.ts +++ b/src/helpers/dom/shake.ts @@ -1,7 +1,8 @@ import rootScope from '../../lib/rootScope'; +import liteMode from '../liteMode'; export default function shake(element: HTMLElement) { - if(!rootScope.settings.animationsEnabled) { + if(!liteMode.isAvailable('animations')) { return; } diff --git a/src/helpers/dom/sortable.ts b/src/helpers/dom/sortable.ts index aebf8038..377f5e50 100644 --- a/src/helpers/dom/sortable.ts +++ b/src/helpers/dom/sortable.ts @@ -8,6 +8,7 @@ import {ScrollableBase} from '../../components/scrollable'; import SwipeHandler from '../../components/swipeHandler'; import IS_TOUCH_SUPPORTED from '../../environment/touchSupport'; import rootScope from '../../lib/rootScope'; +import liteMode from '../liteMode'; import {Middleware} from '../middleware'; import clamp from '../number/clamp'; import safeAssign from '../object/safeAssign'; @@ -156,7 +157,7 @@ export default class Sortable { attachClickEvent(document.body, cancelEvent, {capture: true, once: true}); } - if(rootScope.settings.animationsEnabled) { + if(liteMode.isAvailable('animations')) { await pause(250); } diff --git a/src/helpers/dropdownHover.ts b/src/helpers/dropdownHover.ts index a9707ed2..07b6bb9d 100644 --- a/src/helpers/dropdownHover.ts +++ b/src/helpers/dropdownHover.ts @@ -13,6 +13,7 @@ import safeAssign from './object/safeAssign'; import appNavigationController, {NavigationItem} from '../components/appNavigationController'; import findUpClassName from './dom/findUpClassName'; import rootScope from '../lib/rootScope'; +import liteMode from './liteMode'; const KEEP_OPEN = false; const TOGGLE_TIMEOUT = 200; @@ -171,7 +172,7 @@ export default class DropdownHover extends EventListenerBase<{ return; } - const delay = IS_TOUCH_SUPPORTED || !rootScope.settings.animationsEnabled ? 0 : ANIMATION_DURATION; + const delay = IS_TOUCH_SUPPORTED || !liteMode.isAvailable('animations') ? 0 : ANIMATION_DURATION; if((this.element.style.display && enable === undefined) || enable) { const res = this.dispatchResultableEvent('open'); await Promise.all(res); diff --git a/src/helpers/fastSmoothScroll.ts b/src/helpers/fastSmoothScroll.ts index 422fb46f..7fdd0895 100644 --- a/src/helpers/fastSmoothScroll.ts +++ b/src/helpers/fastSmoothScroll.ts @@ -11,6 +11,7 @@ import {fastRafPromise} from './schedulers'; import {animateSingle, cancelAnimationByKey} from './animation'; import rootScope from '../lib/rootScope'; import isInDOM from './dom/isInDOM'; +import liteMode from './liteMode'; const MIN_JS_DURATION = 250; const MAX_JS_DURATION = 600; @@ -58,7 +59,7 @@ export default function fastSmoothScroll(options: ScrollOptions) { options.axis ??= 'y'; // return; - if(!rootScope.settings.animationsEnabled || options.forceDuration === 0) { + if(!liteMode.isAvailable('animations') || options.forceDuration === 0) { options.forceDirection = FocusDirection.Static; } diff --git a/src/helpers/liteMode.ts b/src/helpers/liteMode.ts new file mode 100644 index 00000000..a9084e22 --- /dev/null +++ b/src/helpers/liteMode.ts @@ -0,0 +1,24 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import {MOUNT_CLASS_TO} from '../config/debug'; +import rootScope from '../lib/rootScope'; + +export type LiteModeKey = 'all' | 'gif' | 'video' | + 'emoji' | 'emoji_panel' | 'emoji_messages' | + 'effects' | 'effects_reactions' | 'effects_premiumstickers' | 'effects_emoji' | + 'stickers' | 'stickers_panel' | 'stickers_chat' | + 'chat' | 'chat_background' | 'chat_spoilers' | 'animations'; + +export class LiteMode { + public isAvailable(key: LiteModeKey) { + return !rootScope.settings.liteMode.all && !rootScope.settings.liteMode[key]; + } +} + +const liteMode = new LiteMode(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.liteMode = liteMode); +export default liteMode; diff --git a/src/lang.ts b/src/lang.ts index 07944c9f..081eaa76 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -115,6 +115,17 @@ const lang = { 'Link.Available': 'Link is available', 'Link.Taken': 'Link is already taken', 'Link.Invalid': 'Link is invalid', + 'LiteMode.Key.chat.Title': 'Chat Animations', + 'LiteMode.Key.chat_background.Title': 'Background rotation', + 'LiteMode.Key.chat_spoilers.Title': 'Animated spoiler effect', + 'LiteMode.Key.stickers_panel.Title': 'Autoplay in panel', + 'LiteMode.Key.stickers_chat.Title': 'Autoplay in chat', + 'LiteMode.Key.emoji_panel.Title': 'Autoplay in panel', + 'LiteMode.Key.emoji_messages.Title': 'Autoplay in messages', + 'LiteMode.Key.effects.Title': 'Interactive Effects', + 'LiteMode.Key.effects_reactions.Title': 'Reaction effect', + 'LiteMode.Key.effects_premiumstickers.Title': 'Premium stickers effect', + 'LiteMode.Key.effects_emoji.Title': 'Emoji effect', 'StickersTab.SearchPlaceholder': 'Search Stickers', 'ForwardedFrom': 'Forwarded from %s', 'Popup.Avatar.Title': 'Drag to Reposition', @@ -1178,6 +1189,20 @@ const lang = { 'other_value': 'last seen %d hours ago' }, 'Login.Register.LastName.Placeholder': 'Last Name', + 'LiteMode.Title': 'Power Saving', + 'LiteMode.Key.emoji.Title': 'Emoji Animations', + 'LiteMode.Key.gif.Title': 'Autoplay GIFs', + 'LiteMode.Key.video.Title': 'Autoplay Videos', + 'LiteMode.Key.stickers.Title': 'Sticker Animations', + 'LiteMode.Key.animations.Title': 'Interface Animations', + // 'LiteMode.Key.emoji.Info': 'Loop animated emoji in messages, reactions and statuses.', + // 'LiteMode.Key.gif.Info': 'Autoplay and loop GIFs in chats and in the keyboard.', + // 'LiteMode.Key.video.Info': 'Autoplay and loop videos and video messages in chats.', + // 'LiteMode.Key.stickers.Info': 'Loop animated stickers, in chats and in the keyboard.', + // 'LiteMode.Key.animations.Info': 'Other animations that make Telegram look amazing.', + 'LiteMode.Info': 'Reduce all power-intensive animations and improve performance.', + 'LiteMode.EnableText': 'Power Saving Mode', + 'LiteMode.DisableAlert': 'Disable Power Saving Mode', 'Message.Context.Select': 'Select', 'Message.Context.Pin': 'Pin', 'Message.Context.Unpin': 'Unpin', diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index d633cf05..bb77022c 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -105,6 +105,8 @@ import PopupForward from '../../components/popups/forward'; import AppBackgroundTab from '../../components/sidebarLeft/tabs/background'; import partition from '../../helpers/array/partition'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; +import liteMode, {LiteModeKey} from '../../helpers/liteMode'; +import RLottiePlayer from '../rlottie/rlottiePlayer'; export type ChatSavedPosition = { mids: number[], @@ -458,6 +460,29 @@ export class AppImManager extends EventListenerBase<{ }); }; + document.addEventListener('mousemove', (e) => { + const mediaStickerWrapper = findUpClassName(e.target, 'media-sticker-wrapper'); + if(!mediaStickerWrapper || + mediaStickerWrapper.classList.contains('custom-emoji') || + findUpClassName(e.target, 'emoji-big')) { + return; + } + + const animations = animationIntersector.getAnimations(mediaStickerWrapper); + animations?.forEach((animationItem) => { + const {liteModeKey, animation} = animationItem; + if(!liteModeKey || !animation?.paused || liteMode.isAvailable(liteModeKey)) { + return; + } + + if(animation instanceof RLottiePlayer) { + animation.playOrRestart(); + } else { + animation.play(); + } + }); + }); + rootScope.addEventListener('sticker_updated', ({type, faved}) => { if(type === 'faved') { toastNew({ @@ -1006,7 +1031,7 @@ export class AppImManager extends EventListenerBase<{ private toggleChatGradientAnimation(activatingChat: Chat) { this.chats.forEach((chat) => { if(chat.gradientRenderer) { - chat.gradientRenderer.scrollAnimate(rootScope.settings.animationsEnabled && chat === activatingChat); + chat.gradientRenderer.scrollAnimate(liteMode.isAvailable('animations') && chat === activatingChat); } }); } @@ -1639,18 +1664,21 @@ export class AppImManager extends EventListenerBase<{ }); } - document.body.classList.toggle('animation-level-0', !rootScope.settings.animationsEnabled); + document.body.classList.toggle('animation-level-0', !liteMode.isAvailable('animations')); document.body.classList.toggle('animation-level-1', false); - document.body.classList.toggle('animation-level-2', rootScope.settings.animationsEnabled); + document.body.classList.toggle('animation-level-2', liteMode.isAvailable('animations')); this.chatsSelectTabDebounced = debounce(() => { const topbar = this.chat.topbar; topbar.pinnedMessage?.setCorrectIndex(0); // * буду молиться богам, чтобы это ничего не сломало, но это исправляет получение пиннеда после анимации this.managers.apiFileManager.setQueueId(this.chat.bubbles.lazyLoadQueue.queueId); - }, rootScope.settings.animationsEnabled ? 250 : 0, false, true); + }, liteMode.isAvailable('animations') ? 250 : 0, false, true); - if(lottieLoader.setLoop(rootScope.settings.stickers.loop)) { + const c: LiteModeKey[] = ['stickers_chat', 'stickers_panel']; + const changedLoop = lottieLoader.setLoop(rootScope.settings.stickers.loop); + const changedAutoplay = !!c.filter((key) => lottieLoader.setAutoplay(liteMode.isAvailable(key), key)).length; + if(changedLoop || changedAutoplay) { animationIntersector.checkAnimations2(false); } @@ -1679,7 +1707,7 @@ export class AppImManager extends EventListenerBase<{ this.chatsSelectTabDebounced(); // ! нужно переделать на animation, так как при лаге анимация будет длиться не 250мс - if(rootScope.settings.animationsEnabled && animate !== false) { + if(liteMode.isAvailable('animations') && animate !== false) { dispatchHeavyAnimationEvent(pause(250 + 150), 250 + 150); } @@ -1927,11 +1955,11 @@ export class AppImManager extends EventListenerBase<{ this.log('selectTab', id, prevTabId); - let animationPromise: Promise = rootScope.settings.animationsEnabled ? doubleRaf() : Promise.resolve(); + let animationPromise: Promise = liteMode.isAvailable('animations') ? doubleRaf() : Promise.resolve(); if( prevTabId !== undefined && prevTabId !== id && - rootScope.settings.animationsEnabled && + liteMode.isAvailable('animations') && animate !== false/* && mediaSizes.activeScreen !== ScreenSize.large */ ) { diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts index f86861f9..a30fde9d 100644 --- a/src/lib/richTextProcessor/wrapRichText.ts +++ b/src/lib/richTextProcessor/wrapRichText.ts @@ -87,7 +87,11 @@ export class CustomEmojiElement extends HTMLElement { // } if(this.player) { - animationIntersector.addAnimation(this, this.renderer.animationGroup, undefined, true); + animationIntersector.addAnimation({ + animation: this, + group: this.renderer.animationGroup, + controlled: true + }); } // this.connectedCallback = undefined; @@ -185,7 +189,7 @@ export class CustomEmojiElement extends HTMLElement { this.paused = false; if(this.player instanceof HTMLVideoElement) { - this.player.currentTime = this.renderer.lastPausedVideo?.currentTime || this.player.currentTime; + this.player.currentTime = this.renderer.lastPausedVideo?.currentTime ?? this.player.currentTime; this.player.play().catch(noop); } @@ -206,6 +210,10 @@ export class CustomEmojiElement extends HTMLElement { public get autoplay() { return true; } + + public get loop() { + return true; + } } type CustomEmojiElements = Set; @@ -648,7 +656,11 @@ export class CustomEmojiRendererElement extends HTMLElement { } if(element.isConnected) { - animationIntersector.addAnimation(element, element.renderer.animationGroup, undefined, true); + animationIntersector.addAnimation({ + animation: element, + group: element.renderer.animationGroup, + controlled: true + }); } }); diff --git a/src/lib/rlottie/lottieLoader.ts b/src/lib/rlottie/lottieLoader.ts index 9333d6ef..2a49c6d5 100644 --- a/src/lib/rlottie/lottieLoader.ts +++ b/src/lib/rlottie/lottieLoader.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type {LiteModeKey} from '../../helpers/liteMode'; import animationIntersector, {AnimationItemGroup} from '../../components/animationIntersector'; import {MOUNT_CLASS_TO} from '../../config/debug'; import pause from '../../helpers/schedulers/pause'; @@ -45,6 +46,20 @@ export class LottieLoader { return null; } + public setAutoplay(play: boolean, liteModeKey: LiteModeKey) { + let changed = false; + for(const i in this.players) { + const player = this.players[i]; + if(player.liteModeKey === liteModeKey) { + changed = true; + player.autoplay = play ? !!+player.el[0].dataset.stickerPlay : false; + player.loop = play ? !!+player.el[0].dataset.stickerLoop : false; + } + } + + return changed; + } + public setLoop(loop: boolean) { let changed = false; for(const i in this.players) { @@ -196,7 +211,12 @@ export class LottieLoader { const player = this.initPlayer(containers, params); - animationIntersector.addAnimation(player, group, undefined, middleware); + animationIntersector.addAnimation({ + animation: player, + group, + controlled: middleware, + liteModeKey: params.liteModeKey + }); return player; } diff --git a/src/lib/rlottie/rlottieIcon.ts b/src/lib/rlottie/rlottieIcon.ts index 591311bf..7d91f3e7 100644 --- a/src/lib/rlottie/rlottieIcon.ts +++ b/src/lib/rlottie/rlottieIcon.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import liteMode from '../../helpers/liteMode'; import noop from '../../helpers/noop'; import safeAssign from '../../helpers/object/safeAssign'; import rootScope from '../rootScope'; @@ -181,7 +182,7 @@ export default class RLottieIcon { const part = item.getPart(index); item.player.playPart({ - from: rootScope.settings.animationsEnabled && !this.skipAnimation ? part.startFrame : part.endFrame, + from: liteMode.isAvailable('animations') && !this.skipAnimation ? part.startFrame : part.endFrame, to: part.endFrame, callback }); diff --git a/src/lib/rlottie/rlottiePlayer.ts b/src/lib/rlottie/rlottiePlayer.ts index b0221792..cae1904f 100644 --- a/src/lib/rlottie/rlottiePlayer.ts +++ b/src/lib/rlottie/rlottiePlayer.ts @@ -6,6 +6,7 @@ import type {AnimationItemGroup, AnimationItemWrapper} from '../../components/animationIntersector'; import type {Middleware} from '../../helpers/middleware'; +import type {LiteModeKey} from '../../helpers/liteMode'; import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables'; import IS_APPLE_MX from '../../environment/appleMx'; import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environment/userAgent'; @@ -35,7 +36,8 @@ export type RLottieOptions = { name?: string, skipFirstFrameRendering?: boolean, toneIndex?: number, - sync?: boolean + sync?: boolean, + liteModeKey?: LiteModeKey }; export type RLottieColor = [number, number, number]; @@ -92,6 +94,7 @@ export default class RLottiePlayer extends EventListenerBase<{ public loop: number | boolean = true; public _loop: RLottiePlayer['loop']; // ! will be used to store original value for settings.stickers.loop public group: AnimationItemGroup = ''; + public liteModeKey: LiteModeKey; private frInterval: number; private frThen: number; @@ -153,6 +156,7 @@ export default class RLottiePlayer extends EventListenerBase<{ this.skipFirstFrameRendering = options.skipFirstFrameRendering; this.toneIndex = options.toneIndex; this.raw = this.color !== undefined; + this.liteModeKey = options.liteModeKey; if(this.name) { this.cacheName = RLottiePlayer.CACHE.generateName(this.name, this.width, this.height, this.color, this.toneIndex); @@ -284,6 +288,18 @@ export default class RLottiePlayer extends EventListenerBase<{ this.play(); } + public playOrRestart() { + if(!this.paused) { + return; + } + + if(this.curFrame === this.maxFrame) { + this.restart(); + } else { + this.play(); + } + } + public setSpeed(speed: number) { if(this.speed === speed) { return; diff --git a/src/scss/partials/_checkbox.scss b/src/scss/partials/_checkbox.scss index 3b774ab3..0977e8f2 100644 --- a/src/scss/partials/_checkbox.scss +++ b/src/scss/partials/_checkbox.scss @@ -66,9 +66,9 @@ left: -15%; background-color: var(--primary-color); - transform: scale(1); + transform: scale(0); border-radius: 50%; - transition: transform .2s 0s ease-in-out; + transition: transform .2s .05s ease-in-out; @include animation-level(0) { transition: none !important; @@ -87,10 +87,10 @@ stroke: #fff; stroke-width: 3.75; stroke-linecap: round; - stroke-dasharray: 24.19, 24.19; + stroke-dasharray: 0, 24.19; stroke-dashoffset: 0; - transition: stroke-dasharray .1s .15s ease-in-out, visibility 0s .15s; - visibility: visible; // fix blinking on parent's transform + transition: stroke-dasharray .1s ease-in-out, visibility 0s .1s; + visibility: hidden; // fix blinking on parent's transform @include animation-level(0) { transition: none !important; @@ -287,18 +287,18 @@ } .checkbox-field .checkbox-field-input { - &:not(:checked) + .checkbox-box { + &:checked:not(.is-fake-disabled) + .checkbox-box { .checkbox-box-check { use { - stroke-dasharray: 0, 24.19; - visibility: hidden; - transition: stroke-dasharray .1s ease-in-out, visibility 0s .1s; + stroke-dasharray: 24.19, 24.19; + visibility: visible; + transition: stroke-dasharray .1s .15s ease-in-out, visibility 0s .15s; } } .checkbox-box-background { - transition: transform .2s .05s ease-in-out; - transform: scale(0); + transition: transform .2s 0s ease-in-out; + transform: scale(1); } } @@ -402,7 +402,7 @@ } } - [type="checkbox"]:checked + .checkbox-toggle { + [type="checkbox"]:checked:not(.is-fake-disabled) + .checkbox-toggle { background-color: var(--primary-color); &:before { diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index c5204fb0..a9adef56 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -1204,6 +1204,12 @@ } } +.power-saving-container { + .row.is-disabled { + + } +} + .empty-placeholder { // left: 50%; // transform: translate(-50%, -50%);