diff --git a/src/components/chat/bubbleGroups.ts b/src/components/chat/bubbleGroups.ts index 1eaa25b4c..09c3ac96e 100644 --- a/src/components/chat/bubbleGroups.ts +++ b/src/components/chat/bubbleGroups.ts @@ -282,7 +282,7 @@ export default class BubbleGroups { } removeItem(item: GroupItem) { - item.group.removeItem(item); + item.group?.removeItem(item); this.removeItemFromCache(item); } @@ -298,10 +298,12 @@ export default class BubbleGroups { const group = item.group; this.removeItem(item); - group.unmountItem(item); const modifiedGroups: Set = new Set(); - modifiedGroups.add(group); + if(group) { + group.unmountItem(item); + modifiedGroups.add(group); + } const [previousSibling, nextSibling] = siblings; if( diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 9362ac6b1..c4f4a27cd 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1214,7 +1214,7 @@ export default class ChatBubbles { private onBubblesMouseMove = async(e: MouseEvent) => { const content = findUpClassName(e.target, 'bubble-content'); - if(content && !this.chat.selection.isSelecting) { + if(content && !this.chat.selection.isSelecting && !findUpClassName(e.target, 'service')) { const bubble = findUpClassName(content, 'bubble'); if(!this.chat.selection.canSelectBubble(bubble)) { this.unhoverPrevious(); @@ -1241,7 +1241,12 @@ export default class ChatBubbles { content.append(hoverReaction); - let message = (await this.chat.getMessage(+bubble.dataset.mid)) as Message.message; + let message = await this.chat.getMessage(+bubble.dataset.mid); + if(message?._ !== 'message') { + this.unhoverPrevious(); + return; + } + message = await this.managers.appMessagesManager.getGroupsFirstMessage(message); const middleware = this.getMiddleware(() => this.hoverReaction === hoverReaction); @@ -1281,7 +1286,7 @@ export default class ChatBubbles { attachClickEvent(hoverReaction, (e) => { cancelEvent(e); // cancel triggering selection - this.managers.appReactionsManager.sendReaction(message, availableReaction); + this.managers.appReactionsManager.sendReaction(message as Message.message, availableReaction); this.unhoverPrevious(); }, {listenerSetter: this.listenerSetter}); }, noop); @@ -4407,6 +4412,7 @@ export default class ChatBubbles { bubble.classList.add('must-have-name'); } + const isForward = fwdFromId || fwdFrom; if(isHidden) { // /////this.log('message to render hidden', message); title = document.createElement('span'); @@ -4415,7 +4421,7 @@ export default class ChatBubbles { // title = fwdFrom.from_name; bubble.classList.add('hidden-profile'); } else { - title = new PeerTitle({peerId: fwdFromId || message.fromId, withPremiumIcon: true}).element; + title = new PeerTitle({peerId: fwdFromId || message.fromId, withPremiumIcon: !isForward}).element; } if(message.reply_to_mid && message.reply_to_mid !== this.chat.threadId && isMessage) { @@ -4430,7 +4436,7 @@ export default class ChatBubbles { // this.log(title); let nameDiv: HTMLElement; - if((fwdFromId || fwdFrom)) { + if(isForward) { if(this.peerId !== rootScope.myId && !isForwardFromChannel) { bubble.classList.add('forwarded'); } diff --git a/src/components/dialogsContextMenu.ts b/src/components/dialogsContextMenu.ts index 4c8c633f8..146ce213a 100644 --- a/src/components/dialogsContextMenu.ts +++ b/src/components/dialogsContextMenu.ts @@ -9,9 +9,9 @@ import type {Dialog} from '../lib/appManagers/appMessagesManager'; import rootScope from '../lib/rootScope'; import ButtonMenu, {ButtonMenuItemOptions} from './buttonMenu'; import PopupDeleteDialog from './popups/deleteDialog'; -import {i18n} from '../lib/langPack'; +import {i18n, LangPackKey, _i18n} from '../lib/langPack'; import findUpTag from '../helpers/dom/findUpTag'; -import PopupPeer from './popups/peer'; +import PopupPeer, {PopupPeerButton} from './popups/peer'; import AppChatFoldersTab from './sidebarLeft/tabs/chatFolders'; import appSidebarLeft from './sidebarLeft'; import {toastNew} from './toast'; @@ -19,6 +19,7 @@ import PopupMute from './popups/mute'; import {AppManagers} from '../lib/appManagers/managers'; import positionMenu from '../helpers/positionMenu'; import contextMenuController from '../helpers/contextMenuController'; +import type {ApiLimitType} from '../lib/mtproto/api_methods'; export default class DialogsContextMenu { private element: HTMLElement; @@ -113,6 +114,107 @@ export default class DialogsContextMenu { if(this.filterId >= 1) { toastNew({langPackKey: 'PinFolderLimitReached'}); } else { + // const a: {[type in ApiLimitType]?: { + // title: LangPackKey, + // description: LangPackKey, + // descriptionPremium: LangPackKey, + // descriptionLocked: LangPackKey, + // icon: string + // }} = { + // pin: { + // title: 'LimitReached', + // description: 'LimitReachedPinDialogs', + // descriptionPremium: 'LimitReachedPinDialogsPremium', + // descriptionLocked: 'LimitReachedPinDialogsLocked', + // icon: 'limit_pin' + // } + // }; + + // class P extends PopupPeer { + // constructor(options: { + // isPremium: boolean, + // limit: number, + // limitPremium: number + // }, _a: typeof a[keyof typeof a]) { + // super('popup-limit', { + // buttons: options.isPremium === undefined ? [{ + // langKey: 'LimitReached.Ok', + // isCancel: true + // }] : (options.isPremium ? [{ + // langKey: 'OK', + // isCancel: true + // }] : [{ + // langKey: 'IncreaseLimit', + // callback: () => { + + // } + // }, { + // langKey: 'Cancel', + // isCancel: true + // }]), + // descriptionLangKey: options.isPremium === undefined ? _a.descriptionLocked : (options.isPremium ? _a.descriptionPremium : _a.description), + // descriptionLangArgs: options.isPremium ? [options.limitPremium] : [options.limit, options.limitPremium], + // titleLangKey: _a.title + // }); + + // const isLocked = options.isPremium === undefined; + // if(isLocked) { + // this.element.classList.add('is-locked'); + // } + + // const limitContainer = document.createElement('div'); + // limitContainer.classList.add('popup-limit-line'); + + // const hint = document.createElement('div'); + // hint.classList.add('popup-limit-hint'); + // const i = document.createElement('span'); + // i.classList.add('popup-limit-hint-icon', 'tgico-' + _a.icon); + // hint.append(i, '' + (options.isPremium ? options.limitPremium : options.limit)); + + // limitContainer.append(hint); + + // if(!isLocked) { + // const limit = document.createElement('div'); + // limit.classList.add('limit-line'); + + // const free = document.createElement('div'); + // free.classList.add('limit-line-free'); + + // const premium = document.createElement('div'); + // premium.classList.add('limit-line-premium'); + + // limit.append(free, premium); + + // _i18n(free, 'LimitFree'); + // premium.append(i18n('LimitPremium'), '' + options.limitPremium); + + // limitContainer.append(limit); + // } + + // this.container.insertBefore(limitContainer, this.description); + + // if(options.isPremium === false) { + // this.buttons.pop().element.remove(); + // } + // } + // } + + // async function showLimitPopup(type: keyof typeof a) { + // const _a = a[type]; + // const [appConfig, limit, limitPremium] = await Promise.all([ + // rootScope.managers.apiManager.getAppConfig(), + // ...[false, true].map((v) => rootScope.managers.apiManager.getLimit(type, v)) + // ]); + // const isLocked = appConfig.premium_purchase_blocked; + // new P({ + // isPremium: isLocked ? undefined : rootScope.premium, + // limit, + // limitPremium + // }, _a).show(); + // } + + // showLimitPopup('pin'); + const config = await this.managers.apiManager.getConfig(); new PopupPeer('pinned-dialogs-too-much', { buttons: [{ diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts index 022b49723..a59476cd7 100644 --- a/src/components/emoticonsDropdown/tabs/stickers.ts +++ b/src/components/emoticonsDropdown/tabs/stickers.ts @@ -298,8 +298,7 @@ class EmoticonsTabC { export default class StickersTab extends EmoticonsTabC implements EmoticonsTab { private superStickerRenderer: SuperStickerRenderer; - private setFavedLimit(appConfig: MTAppConfig) { - const limit = rootScope.premium ? appConfig.stickers_faved_limit_premium : appConfig.stickers_faved_limit_default; + private setFavedLimit(limit: number) { const category = this.categories['faved']; category.limit = limit; } @@ -494,10 +493,10 @@ export default class StickersTab extends EmoticonsTabC implements EmoticonsTab { const promises = [ Promise.all([ - this.managers.apiManager.getAppConfig(), + this.managers.apiManager.getLimit('favedStickers'), this.managers.appStickersManager.getFavedStickersStickers() - ]).then(([appConfig, stickers]) => { - this.setFavedLimit(appConfig); + ]).then(([limit, stickers]) => { + this.setFavedLimit(limit); onCategoryStickers(favedCategory, stickers); }), @@ -604,7 +603,9 @@ export default class StickersTab extends EmoticonsTabC implements EmoticonsTab { }); rootScope.addEventListener('app_config', (appConfig) => { - this.setFavedLimit(appConfig); + this.managers.apiManager.getLimit('favedStickers').then((limit) => { + this.setFavedLimit(limit); + }); }); const resizeCategories = () => { diff --git a/src/components/row.ts b/src/components/row.ts index 250c810d6..cb2d8163c 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -209,6 +209,7 @@ export const RadioFormFromRows = (rows: Row[], onChange: (value: string) => void export const RadioFormFromValues = (values: {langPackKey: LangPackKey, value: number | string, checked?: boolean}[], onChange: Parameters[1]) => { const name = 'name-' + (Math.random() * 0x7FFFFF | 0); + let checkedRadioField: RadioField; const rows = values.map(({langPackKey, value, checked}) => { const row = new Row({ radioField: new RadioField({ @@ -219,11 +220,15 @@ export const RadioFormFromValues = (values: {langPackKey: LangPackKey, value: nu }); if(checked) { - row.radioField.checked = checked; + checkedRadioField = row.radioField; } return row; }); - return RadioFormFromRows(rows, onChange); + const form = RadioFormFromRows(rows, onChange); + if(checkedRadioField) { + checkedRadioField.checked = true; + } + return form; }; diff --git a/src/components/sidebarLeft/tabs/chatFolders.ts b/src/components/sidebarLeft/tabs/chatFolders.ts index 75a3a88b1..a69d4c9f4 100644 --- a/src/components/sidebarLeft/tabs/chatFolders.ts +++ b/src/components/sidebarLeft/tabs/chatFolders.ts @@ -260,13 +260,13 @@ export default class AppChatFoldersTab extends SliderSuperTab { } private async canCreateFolder() { - const [appConfig, filters] = await Promise.all([ - this.managers.apiManager.getAppConfig(), + const [limit, filters] = await Promise.all([ + this.managers.apiManager.getLimit('folders'), this.managers.filtersStorage.getDialogFilters() ]); const filtersLength = filters.filter((filter) => !REAL_FOLDERS.has(filter.id)).length; - return filtersLength < (rootScope.premium ? appConfig.dialog_filters_limit_premium : appConfig.dialog_filters_limit_default); + return filtersLength < limit; } private getSuggestedFilters() { diff --git a/src/components/sidebarLeft/tabs/editProfile.ts b/src/components/sidebarLeft/tabs/editProfile.ts index ca47b659e..c1dc85766 100644 --- a/src/components/sidebarLeft/tabs/editProfile.ts +++ b/src/components/sidebarLeft/tabs/editProfile.ts @@ -37,7 +37,7 @@ export default class AppEditProfileTab extends SliderSuperTab { const inputWrapper = document.createElement('div'); inputWrapper.classList.add('input-wrapper'); - const appConfig = await this.managers.apiManager.getAppConfig(); + const bioMaxLength = await this.managers.apiManager.getLimit('bio'); this.firstNameInputField = new InputField({ label: 'EditProfile.FirstNameLabel', name: 'first-name', @@ -51,7 +51,7 @@ export default class AppEditProfileTab extends SliderSuperTab { this.bioInputField = new InputField({ label: 'EditProfile.BioLabel', name: 'bio', - maxLength: rootScope.premium ? appConfig.about_length_limit_premium : appConfig.about_length_limit_default + maxLength: bioMaxLength }); inputWrapper.append(this.firstNameInputField.container, this.lastNameInputField.container, this.bioInputField.container); diff --git a/src/config/app.ts b/src/config/app.ts index 2a538b1f7..969dc1bdc 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -19,7 +19,7 @@ const App = { version: process.env.VERSION, versionFull: process.env.VERSION_FULL, build: +process.env.BUILD, - langPackVersion: '0.4.8', + langPackVersion: '0.4.9', langPack: 'macos', langPackCode: 'en', domains: [MAIN_DOMAIN] as string[], diff --git a/src/lang.ts b/src/lang.ts index c8f61998c..a16f6b268 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -142,6 +142,7 @@ const lang = { 'PaymentInfo.Done': 'PROCEED TO CHECKOUT', 'PaymentCard.Error.Invalid': 'Invalid card number', 'PaymentCard.Error.Incomplete': 'Incomplete card number', + 'LimitReached.Ok': 'OK, GOT IT', // * android 'AccDescrEditing': 'Editing', @@ -783,6 +784,13 @@ const lang = { 'AllReactions': 'All reactions', 'SomeReactions': 'Some reactions', 'NoReactions': 'No reactions', + 'LimitReached': 'Limit Reached', + 'LimitReachedPinDialogs': 'You can\'t pin more than %1$d chats to the top. Unpin some that are currently pinned – or subscribe to **Telegram Premium** to double the limit to **%2$d** chats.', + 'LimitReachedPinDialogsPremium': 'Sorry, you can\'t pin more than %1$d chats to the top. Unpin some that are currently pinned.', + 'LimitReachedPinDialogsLocked': 'Sorry, you can\'t pin more than %1$d chats to the top. Unpin some that are currently pinned. We are working to let you increase this limit in the future.', + 'IncreaseLimit': 'Increase Limit', + 'LimitFree': 'Free', + 'LimitPremium': 'Premium', // * macos 'AccountSettings.Filters': 'Chat Folders', diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index a00dd3066..154e3b445 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -3085,10 +3085,7 @@ export class AppMessagesManager extends AppManager { const pinned = dialog.pFlags?.pinned ? undefined : true; if(pinned) { - const appConfig = await this.apiManager.getAppConfig(); - const max = filterId === 1 ? - (this.rootScope.premium ? appConfig.dialogs_folder_pinned_limit_premium : appConfig.dialogs_folder_pinned_limit_default) : - (this.rootScope.premium ? appConfig.dialogs_pinned_limit_premium : appConfig.dialogs_pinned_limit_default); + const max = await this.apiManager.getLimit(filterId === 1 ? 'folderPin' : 'pin'); if(this.dialogsStorage.getPinnedOrders(filterId).length >= max) { return Promise.reject(makeError('PINNED_DIALOGS_TOO_MUCH')); } @@ -4329,8 +4326,11 @@ export class AppMessagesManager extends AppManager { message }); - if(isTopMessage || (message as Message.message).grouped_id) { + if(isTopMessage) { this.dialogsStorage.setDialogToState(dialog); + } + + if((isTopMessage || (message as Message.message).grouped_id) && dialog) { this.rootScope.dispatchEvent('dialogs_multiupdate', new Map([[peerId, dialog]])); } } diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index aedd27fc9..67a0f6764 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -22,10 +22,13 @@ import {ReferenceContext} from '../mtproto/referenceDatabase'; export type UserTyping = Partial<{userId: UserId, action: SendMessageAction, timeout: number}>; +const PEER_FULL_TTL = 3 * 60e3; + export class AppProfileManager extends AppManager { // private botInfos: any = {}; private usersFull: {[id: UserId]: UserFull.userFull} = {}; private chatsFull: {[id: ChatId]: ChatFull} = {}; + private fullExpiration: {[peerId: PeerId]: number} = {}; private typingsInPeer: {[peerId: PeerId]: UserTyping[]}; protected after() { @@ -161,7 +164,7 @@ export class AppProfileManager extends AppManager { } */ public getProfile(id: UserId, override?: true) { - if(this.usersFull[id] && !override) { + if(this.usersFull[id] && !override && Date.now() < this.fullExpiration[id.toPeerId()]) { return this.usersFull[id]; } @@ -193,6 +196,7 @@ export class AppProfileManager extends AppManager { }); this.usersFull[id] = userFull; + this.fullExpiration[peerId] = Date.now() + PEER_FULL_TTL; /* if(userFull.bot_info) { userFull.bot_info = this.saveBotInfo(userFull.bot_info) as any; @@ -263,9 +267,10 @@ export class AppProfileManager extends AppManager { return this.getChannelFull(id, override); } + const peerId = id.toPeerId(true); const fullChat = this.chatsFull[id] as ChatFull.chatFull; - if(fullChat && !override) { - const chat = this.appChatsManager.getChat(id); + if(fullChat && !override && Date.now() < this.fullExpiration[peerId]) { + const chat: Chat.chat = this.appChatsManager.getChat(id); if(chat.version === (fullChat.participants as ChatParticipants.chatParticipants).version || chat.pFlags.left) { return fullChat as ChatFull; @@ -281,7 +286,6 @@ export class AppProfileManager extends AppManager { this.appChatsManager.saveApiChats(result.chats, true); this.appUsersManager.saveApiUsers(result.users); const chatFull = result.full_chat as ChatFull.chatFull; - const peerId = id.toPeerId(true); if(chatFull && chatFull.chat_photo && chatFull.chat_photo.id) { chatFull.chat_photo = this.appPhotosManager.savePhoto(chatFull.chat_photo, {type: 'profilePhoto', peerId}); } @@ -297,6 +301,7 @@ export class AppProfileManager extends AppManager { }); this.chatsFull[id] = chatFull; + this.fullExpiration[peerId] = Date.now() + PEER_FULL_TTL; this.rootScope.dispatchEvent('chat_full_update', id); return chatFull; @@ -383,7 +388,8 @@ export class AppProfileManager extends AppManager { } public getChannelFull(id: ChatId, override?: true) { - if(this.chatsFull[id] !== undefined && !override) { + const peerId = id.toPeerId(true); + if(this.chatsFull[id] !== undefined && !override && Date.now() < this.fullExpiration[peerId]) { return this.chatsFull[id] as ChatFull.channelFull; } @@ -393,7 +399,6 @@ export class AppProfileManager extends AppManager { channel: this.appChatsManager.getChannelInput(id) }, processResult: (result) => { - const peerId = id.toPeerId(true); this.appChatsManager.saveApiChats(result.chats, true); this.appUsersManager.saveApiUsers(result.users); const fullChannel = result.full_chat as ChatFull.channelFull; @@ -412,6 +417,7 @@ export class AppProfileManager extends AppManager { }); this.chatsFull[id] = fullChannel; + this.fullExpiration[peerId] = Date.now() + PEER_FULL_TTL; this.rootScope.dispatchEvent('chat_full_update', id); return fullChannel; @@ -533,7 +539,7 @@ export class AppProfileManager extends AppManager { this.rootScope.dispatchEvent('peer_bio_edit', peerId); } - return this.getProfile(this.appPeersManager.peerId, true); + return this.getProfile(user.id, true); }); } diff --git a/src/lib/appManagers/appReactionsManager.ts b/src/lib/appManagers/appReactionsManager.ts index 7d5455779..819c05f02 100644 --- a/src/lib/appManagers/appReactionsManager.ts +++ b/src/lib/appManagers/appReactionsManager.ts @@ -231,8 +231,7 @@ export class AppReactionsManager extends AppManager { }; } - const appConfig = await this.apiManager.getAppConfig(); - const limit = this.rootScope.premium ? appConfig.reactions_user_max_premium : appConfig.reactions_user_max_default; + const limit = await this.apiManager.getLimit('reactions'); const lastSendingTimeKey = message.peerId + '_' + message.mid; const lastSendingTime = this.lastSendingTimes.get(lastSendingTimeKey); diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 5729497f5..b0a673712 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -521,9 +521,8 @@ export class AppStickersManager extends AppManager { return this.getFavedStickers().then(() => this.favedStickers); } - public async getFavedStickersLimit() { - const appConfig = await this.apiManager.getAppConfig(); - return this.rootScope.premium ? appConfig.stickers_faved_limit_premium : appConfig.stickers_faved_limit_default; + public getFavedStickersLimit() { + return this.apiManager.getLimit('favedStickers'); } public async faveSticker(docId: DocId, unfave?: boolean) { diff --git a/src/lib/mtproto/api_methods.ts b/src/lib/mtproto/api_methods.ts index 697aed0ca..51b424711 100644 --- a/src/lib/mtproto/api_methods.ts +++ b/src/lib/mtproto/api_methods.ts @@ -5,6 +5,7 @@ */ import ctx from '../../environment/ctx'; +import callbackify from '../../helpers/callbackify'; import {ignoreRestrictionReasons} from '../../helpers/restrictions'; import {Config, MethodDeclMap, User} from '../../layer'; import {InvokeApiOptions} from '../../types'; @@ -22,6 +23,8 @@ type HashOptions = { [queryJSON: string]: HashResult }; +export type ApiLimitType = 'pin' | 'folderPin' | 'folders' | 'favedStickers' | 'reactions' | 'bio'; + export default abstract class ApiManagerMethods extends AppManager { private afterMessageIdTemp: number; private hashes: {[method: string]: HashOptions} = {}; @@ -282,4 +285,22 @@ export default abstract class ApiManagerMethods extends AppManager { options: {overwrite} }); } + + public getLimit(type: ApiLimitType, isPremium?: boolean) { + return callbackify(this.getAppConfig(), (appConfig) => { + const map: {[type in ApiLimitType]: [keyof MTAppConfig, keyof MTAppConfig]} = { + pin: ['dialogs_pinned_limit_default', 'dialogs_pinned_limit_premium'], + folderPin: ['dialogs_folder_pinned_limit_default', 'dialogs_folder_pinned_limit_premium'], + folders: ['dialog_filters_limit_default', 'dialog_filters_limit_premium'], + favedStickers: ['stickers_faved_limit_default', 'stickers_faved_limit_premium'], + reactions: ['reactions_user_max_default', 'reactions_user_max_premium'], + bio: ['about_length_limit_default', 'about_length_limit_premium'] + }; + + isPremium ??= this.rootScope.premium; + + const key = map[type][isPremium ? 1 : 0]; + return appConfig[key] as number; + }); + } } diff --git a/src/lib/mtproto/appConfig.d.ts b/src/lib/mtproto/appConfig.d.ts index 1d2bcb748..f6ab9ae56 100644 --- a/src/lib/mtproto/appConfig.d.ts +++ b/src/lib/mtproto/appConfig.d.ts @@ -57,6 +57,7 @@ export interface MTAppConfig { message_animated_emoji_max?: number; premium_promo_order?: string[]; premium_bot_username?: string; + premium_purchase_blocked?: boolean; } export interface EmojiesSendDiceSuccess { diff --git a/src/lib/rlottie/rlottie.worker.ts b/src/lib/rlottie/rlottie.worker.ts index f755c50ed..db68a57fa 100644 --- a/src/lib/rlottie/rlottie.worker.ts +++ b/src/lib/rlottie/rlottie.worker.ts @@ -34,10 +34,10 @@ export class RLottieItem { constructor( private reqId: number, private width: number, - private height: number/* , + private height: number, + private raw?: boolean/* , private canvas: OffscreenCanvas */ ) { - } public init(json: string, fps: number) { @@ -67,7 +67,7 @@ export class RLottieItem { reply(['loaded', this.reqId, this.frameCount, this.fps]); - if(IS_IMAGE_BITMAP_SUPPORTED) { + if(!this.raw && IS_IMAGE_BITMAP_SUPPORTED) { this.imageData = new ImageData(this.width, this.height); } } catch(e) { @@ -105,7 +105,7 @@ export class RLottieItem { // this.context.putImageData(new ImageData(clamped, this.width, this.height), 0, 0); - reply(['frame', this.reqId, frameNo, clamped], [clamped]); + reply(['frame', this.reqId, frameNo, clamped], [clamped.buffer]); } } catch(e) { console.error('Render error:', e); @@ -158,8 +158,8 @@ _Module.onRuntimeInitialized = function() { const items: {[reqId: string]: RLottieItem} = {}; const queryableFunctions = { - loadFromData: function(reqId: number, blob: Blob, width: number, height: number, toneIndex: number/* , canvas: OffscreenCanvas */) { - const item = items[reqId] = new RLottieItem(reqId, width, height/* , canvas */); + loadFromData: function(reqId: number, blob: Blob, width: number, height: number, toneIndex: number, raw: boolean/* , canvas: OffscreenCanvas */) { + const item = items[reqId] = new RLottieItem(reqId, width, height, raw/* , canvas */); readBlobAsText(blob).then((json) => { try { if(typeof(toneIndex) === 'number' && toneIndex >= 1 && toneIndex <= 5) { diff --git a/src/lib/rlottie/rlottiePlayer.ts b/src/lib/rlottie/rlottiePlayer.ts index ed987b9ff..4d756bcb5 100644 --- a/src/lib/rlottie/rlottiePlayer.ts +++ b/src/lib/rlottie/rlottiePlayer.ts @@ -120,6 +120,8 @@ export default class RLottiePlayer extends EventListenerBase<{ public overrideRender: (frame: ImageData | HTMLCanvasElement | ImageBitmap) => void; private renderedFirstFrame: boolean; + private raw: boolean; + constructor({el, worker, options}: { el: RLottiePlayer['el'], worker: QueryableWorker, @@ -148,6 +150,7 @@ export default class RLottiePlayer extends EventListenerBase<{ this.name = options.name; this.skipFirstFrameRendering = options.skipFirstFrameRendering; this.toneIndex = options.toneIndex; + this.raw = this.color !== undefined; if(this.name) { this.cacheName = RLottiePlayer.CACHE.generateName(this.name, this.width, this.height, this.color, this.toneIndex); @@ -203,7 +206,7 @@ export default class RLottiePlayer extends EventListenerBase<{ this.contexts = this.canvas.map((canvas) => canvas.getContext('2d')); - if(!IS_IMAGE_BITMAP_SUPPORTED) { + if(!IS_IMAGE_BITMAP_SUPPORTED || this.raw) { this.imageData = new ImageData(this.width, this.height); if(CAN_USE_TRANSFERABLES) { @@ -230,12 +233,12 @@ export default class RLottiePlayer extends EventListenerBase<{ this.cache.clearCache(); } - public sendQuery(args: any[]) { - this.worker.sendQuery([args.shift(), this.reqId, ...args]); + public sendQuery(args: any[], transfer?: Transferable[]) { + this.worker.sendQuery([args.shift(), this.reqId, ...args], transfer); } public loadFromData(data: RLottieOptions['animationData']) { - this.sendQuery(['loadFromData', data, this.width, this.height, this.toneIndex/* , this.canvas.transferControlToOffscreen() */]); + this.sendQuery(['loadFromData', data, this.width, this.height, this.toneIndex, this.color !== undefined/* , this.canvas.transferControlToOffscreen() */]); } public play() { @@ -442,7 +445,7 @@ export default class RLottiePlayer extends EventListenerBase<{ this.clamped = new Uint8ClampedArray(this.width * this.height * 4); } - this.sendQuery(['renderFrame', frameNo, this.clamped]); + this.sendQuery(['renderFrame', frameNo], this.clamped ? [this.clamped.buffer] : undefined); } } diff --git a/src/lib/storages/filters.ts b/src/lib/storages/filters.ts index cae1eb792..90c2ed2e6 100644 --- a/src/lib/storages/filters.ts +++ b/src/lib/storages/filters.ts @@ -545,13 +545,8 @@ export default class FiltersStorage extends AppManager { return true; } - const isPremium = this.rootScope.premium; - let isFolderAvailable = isPremium; - if(!isPremium) { - const config = await this.apiManager.getAppConfig(); - const limit = config.dialog_filters_limit_default; - isFolderAvailable = this.filtersArr.filter((filter) => !REAL_FOLDERS.has(filter.id)).slice(0, limit).some((filter) => filter.id === filterId); - } + const limit = await this.apiManager.getLimit('folders'); + const isFolderAvailable = this.filtersArr.filter((filter) => !REAL_FOLDERS.has(filter.id)).slice(0, limit).some((filter) => filter.id === filterId); return isFolderAvailable; } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index b2f66ec5d..530eec35e 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -6,7 +6,10 @@ @use "sass:math"; -$bubble-margin: .25rem; +$bubble-margin: .125rem; +$bubble-margin-big: .5rem; +$bubble-overflow: math.div($bubble-margin-big, 8); +$bubble-overflow-big: math.div($bubble-margin-big, 2); $bubble-beside-button-width: 38px; @keyframes bubbleSelected { @@ -36,7 +39,7 @@ $bubble-beside-button-width: 38px; e.g. make the height bigger and adjust the top so observeHeaders()'s IntersectionObserver fires as soon as the bottom of the sentinel crosses the top of the intersection container. */ - height: $bubble-margin; + height: $bubble-overflow-big; top: 0; } @@ -136,8 +139,8 @@ $bubble-beside-button-width: 38px; left: -50%; /* top: 0; bottom: 0; */ - top: -#{math.div($bubble-margin, 2)}; - bottom: -#{math.div($bubble-margin, 2)}; + top: -#{$bubble-overflow}; + bottom: -#{$bubble-overflow}; content: " "; z-index: -1; } @@ -145,13 +148,13 @@ $bubble-beside-button-width: 38px; /* &.is-highlighted, &.is-selected { &:not(.is-group-last):after { - height: calc(100% + #{math.div($bubble-margin, 2)}) !important; + height: calc(100% + #{$bubble-overflow}) !important; } & + &:not(.is-group-last) { &:after { top: .125rem !important; - height: calc(100% - #{math.div($bubble-margin, 2)}) !important; + height: calc(100% - #{$bubble-overflow}) !important; } } } */ @@ -159,7 +162,7 @@ $bubble-beside-button-width: 38px; // ! if turn this on, there will be an empty space /* &.is-highlighted, &.is-selected { &.is-group-last:after { - bottom: #{math.div($bubble-margin, 2)} !important; + bottom: #{$bubble-overflow} !important; } } */ @@ -180,7 +183,7 @@ $bubble-beside-button-width: 38px; &:before { content: "Unread messages"; height: 30px; - margin-bottom: $bubble-margin; + margin-bottom: $bubble-overflow-big; margin-left: -50%; margin-right: -50%; text-align: center; @@ -193,9 +196,10 @@ $bubble-beside-button-width: 38px; position: relative; } - &.is-highlighted, &.is-selected { + &.is-highlighted, + &.is-selected { &:after { - top: calc(#{math.div($bubble-margin, 2)} + 30px); + top: calc(#{$bubble-overflow} + 30px); } } } @@ -391,8 +395,8 @@ $bubble-beside-button-width: 38px; &.is-date { position: sticky; - top: $bubble-margin; - padding-bottom: $bubble-margin; + top: $bubble-overflow-big; + padding-bottom: #{$bubble-overflow-big + $bubble-margin}; //z-index: 3; z-index: 2; transition: opacity .3s ease; @@ -500,10 +504,10 @@ $bubble-beside-button-width: 38px; } */ &.is-group-last { - margin-bottom: #{$bubble-margin * 2}; + margin-bottom: #{$bubble-margin-big}; &:after { - bottom: -#{$bubble-margin}; + bottom: -#{$bubble-overflow-big}; } /* > .bubble-select-checkbox { @@ -519,7 +523,7 @@ $bubble-beside-button-width: 38px; &.is-group-first { &:after { - top: -#{$bubble-margin}; + top: -#{$bubble-overflow-big}; } } @@ -1618,7 +1622,7 @@ $bubble-beside-button-width: 38px; &:first-of-type { .document-selection { - top: -#{math.div($bubble-margin, 2)}; // * padding inner + half padding outer + top: -#{$bubble-overflow}; // * padding inner + half padding outer } .document-wrapper { @@ -1630,7 +1634,7 @@ $bubble-beside-button-width: 38px; &:last-of-type { .document-selection { - bottom: -#{math.div($bubble-margin, 2)}; + bottom: -#{$bubble-overflow}; } .document-wrapper { @@ -1644,7 +1648,7 @@ $bubble-beside-button-width: 38px; &.is-group-first .document-container { &:first-of-type { .document-selection { - top: -$bubble-margin; + top: -$bubble-overflow-big; } } } @@ -1652,7 +1656,7 @@ $bubble-beside-button-width: 38px; &.is-group-last .document-container { &:last-of-type { .document-selection { - bottom: -$bubble-margin; + bottom: -$bubble-overflow-big; } } } diff --git a/src/scss/partials/popups/_limit.scss b/src/scss/partials/popups/_limit.scss new file mode 100644 index 000000000..53e423f51 --- /dev/null +++ b/src/scss/partials/popups/_limit.scss @@ -0,0 +1,70 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +.popup-limit { + .popup-container { + min-width: 22.5rem; + } + + &-line { + display: flex; + flex-direction: column; + align-items: center; + margin: .75rem 0; + } + + &-hint { + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-bold); + + &-icon { + font-size: 1.25rem; + margin-right: .25rem; + display: flex; + } + } + + .limit-line { + align-self: stretch; + margin: 1rem .5rem 0; + } + + &:not(.is-locked) &-hint { + height: 2rem; + border-radius: 1rem; + padding: 0 .75rem; + position: relative; + background: linear-gradient(73.4deg, #6C93FF -7.21%, #976FFF 114.57%, #DF69D1 241.52%); + background-size: 200px 2rem; + + &:after { + display: block; + content: " "; + width: 100%; // 26 + height: 9px; + position: absolute; + bottom: -9px; + clip-path: path("M0 0H26H24.4853C22.894 0 21.3679 0.632141 20.2426 1.75736L14.4142 7.58579C13.6332 8.36684 12.3668 8.36683 11.5858 7.58579L5.75736 1.75736C4.63214 0.632139 3.10602 0 1.51472 0H0Z"); + background: inherit; + background-size: inherit; + background-position-x: calc(-50% + -86px); + background-repeat: no-repeat; + left: 50%; + margin-left: -13px; + } + } + + &.is-locked &-hint { + height: 2.75rem; + border-radius: 1.375rem; + padding: 0 1.25rem 0 1rem; + margin-right: -.25rem; + background-color: var(--primary-color); + } +} diff --git a/src/scss/style.scss b/src/scss/style.scss index 31f1a1f50..e324ce845 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -390,6 +390,7 @@ $chat-input-inner-padding-handhelds: .25rem; @import "partials/popups/paymentShippingMethods"; @import "partials/popups/paymentVerification"; @import "partials/popups/paymentCardConfirmation"; +@import "partials/popups/limit"; @import "partials/pages/pages"; @import "partials/pages/authCode"; @@ -1717,6 +1718,43 @@ hr { } } +.limit-line { + height: 2rem; + // border-radius: $border-radius-medium; + font-weight: var(--font-weight-bold); + display: flex; + // background: linear-gradient(84.4deg, #6C93FF -4.85%, #976FFF 51.72%, #DF69D1 110.7%), #F1F3F5; + + &-free { + color: #000; + } + + &-premium { + color: #fff; + } + + &-free { + border-top-left-radius: $border-radius-medium; + border-bottom-left-radius: $border-radius-medium; + background-color: #F1F3F5; + } + + &-premium { + border-top-right-radius: $border-radius-medium; + border-bottom-right-radius: $border-radius-medium; + background: linear-gradient(84.4deg, #6C93FF -4.85%, #976FFF 51.72%, #DF69D1 110.7%); + } + + &-free, + &-premium { + flex: 1 1 0; + padding: 0 .75rem; + display: flex; + align-items: center; + justify-content: space-between; + } +} + .movable-element { --size: .5rem; position: relative;