diff --git a/public/assets/fonts/tgico.svg b/public/assets/fonts/tgico.svg index 8bdaa672..31c5bb4c 100644 --- a/public/assets/fonts/tgico.svg +++ b/public/assets/fonts/tgico.svg @@ -87,135 +87,136 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/fonts/tgico.ttf b/public/assets/fonts/tgico.ttf index eede64f8..2a9f4501 100644 Binary files a/public/assets/fonts/tgico.ttf and b/public/assets/fonts/tgico.ttf differ diff --git a/public/assets/fonts/tgico.woff b/public/assets/fonts/tgico.woff index f987876d..2b4717ae 100644 Binary files a/public/assets/fonts/tgico.woff and b/public/assets/fonts/tgico.woff differ diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 8669bd3b..a34f1d4a 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -29,6 +29,7 @@ import PopupForward from './popups/forward'; import Scrollable from './scrollable'; import appSidebarRight from './sidebarRight'; import AppSharedMediaTab from './sidebarRight/tabs/sharedMedia'; +import PopupElement from './popups'; type AppMediaViewerTargetType = { element: HTMLElement, @@ -193,7 +194,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet onDeleteClick = () => { const target = this.target; - new PopupDeleteMessages(target.peerId, [target.mid], 'chat', () => { + PopupElement.createPopup(PopupDeleteMessages, target.peerId, [target.mid], 'chat', () => { this.target = {element: this.content.media} as any; this.close(); }); @@ -203,7 +204,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet const target = this.target; if(target.mid) { // appSidebarRight.forwardTab.open([target.mid]); - new PopupForward({ + PopupElement.createPopup(PopupForward, { [target.peerId]: [target.mid] }, () => { return this.close(); diff --git a/src/components/appMediaViewerNew.ts b/src/components/appMediaViewerNew.ts index d47b69cf..25c46809 100644 --- a/src/components/appMediaViewerNew.ts +++ b/src/components/appMediaViewerNew.ts @@ -1151,7 +1151,7 @@ // onForwardClick = () => { // if(this.currentMessageId) { // //appSidebarRight.forwardTab.open([this.currentMessageId]); -// new PopupForward(this.currentPeerId, [this.currentMessageId], () => { +// PopupElement.createPopup(PopupForward(this.currentPeerId, [this.currentMessageId], , ) => { // return this.close(); // }); // } diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index c5de3257..4477c4d5 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -77,6 +77,7 @@ import noop from '../helpers/noop'; import wrapMediaSpoiler, {onMediaSpoilerClick} from './wrappers/mediaSpoiler'; import filterAsync from '../helpers/array/filterAsync'; import ChatContextMenu from './chat/contextMenu'; +import PopupElement from './popups'; // const testScroll = false; @@ -257,7 +258,7 @@ class SearchContextMenu { if(this.searchSuper.selection.isSelecting) { simulateClickEvent(this.searchSuper.selection.selectionForwardBtn); } else { - new PopupForward({ + PopupElement.createPopup(PopupForward, { [this.peerId]: [this.mid] }); } @@ -275,7 +276,7 @@ class SearchContextMenu { if(this.searchSuper.selection.isSelecting) { simulateClickEvent(this.searchSuper.selection.selectionDeleteBtn); } else { - new PopupDeleteMessages(this.peerId, [this.mid], 'chat'); + PopupElement.createPopup(PopupDeleteMessages, this.peerId, [this.mid], 'chat'); } }; } diff --git a/src/components/button.ts b/src/components/button.ts index e371bfff..dd6640e8 100644 --- a/src/components/button.ts +++ b/src/components/button.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import {i18n, LangPackKey} from '../lib/langPack'; +import {FormatterArguments, i18n, LangPackKey} from '../lib/langPack'; import ripple from './ripple'; export type ButtonOptions = Partial<{ @@ -13,6 +13,7 @@ export type ButtonOptions = Partial<{ icon: string, rippleSquare: true, text: LangPackKey, + textArgs?: FormatterArguments, disabled: boolean, asDiv: boolean, asLink: boolean @@ -39,7 +40,7 @@ export default function Button(className: string, optio } if(options.text) { - button.append(i18n(options.text)); + button.append(i18n(options.text, options.textArgs)); } return button as any; diff --git a/src/components/buttonCorner.ts b/src/components/buttonCorner.ts index 5d5a49ff..00a26f31 100644 --- a/src/components/buttonCorner.ts +++ b/src/components/buttonCorner.ts @@ -8,6 +8,7 @@ import Button from './button'; const ButtonCorner = (options: Partial<{className: string, icon: string, noRipple: true, onlyMobile: true, asDiv: boolean}> = {}) => { const button = Button('btn-circle btn-corner z-depth-1' + (options.className ? ' ' + options.className : ''), options); + button.tabIndex = -1; return button; }; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 798a2ec1..3e3c89f5 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -785,13 +785,30 @@ export default class ChatBubbles { const bubble = this.bubbles[mid]; if(!bubble) return; - const message = (await this.chat.getMessage(mid)) as Message.message; + const [message, originalMessage] = await Promise.all([ + (await this.chat.getMessage(mid)) as Message.message, + (await this.managers.appMessagesManager.getMessageByPeer(replyToPeerId, replyMid)) as Message.message + ]); + if(!middleware()) return; MessageRender.setReply({ chat: this.chat, bubble, message }); + + let maxMediaTimestamp: number; + const timestamps = bubble.querySelectorAll('.timestamp'); + if(originalMessage && (maxMediaTimestamp = getMediaDurationFromMessage(originalMessage))) { + timestamps.forEach((timestamp) => { + const value = +timestamp.dataset.timestamp; + if(value < maxMediaTimestamp) { + timestamp.classList.remove('is-disabled'); + } else { + timestamp.removeAttribute('href'); + } + }); + } }); }); }); @@ -1787,7 +1804,8 @@ export default class ChatBubbles { return; } - new PopupPayment( + PopupElement.createPopup( + PopupPayment, message as Message.message, await this.managers.appPaymentsManager.getInputInvoiceByPeerId(message.peerId, message.mid) ); @@ -1878,7 +1896,7 @@ export default class ChatBubbles { const message = await this.managers.appMessagesManager.getMessageByPeer(peerId.toPeerId(), +mid); if(message) { const inputInvoice = await this.managers.appPaymentsManager.getInputInvoiceByPeerId(this.peerId, +bubble.dataset.mid); - new PopupPayment(message as Message.message, inputInvoice, undefined, true); + PopupElement.createPopup(PopupPayment, message as Message.message, inputInvoice, undefined, true); } } else { this.chat.appImManager.setInnerPeer({ @@ -1916,7 +1934,7 @@ export default class ChatBubbles { const doc = ((message as Message.message).media as MessageMedia.messageMediaDocument)?.document as Document.document; if(doc?.stickerSetInput) { - new PopupStickers(doc.stickerSetInput).show(); + PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show(); } return; @@ -1941,7 +1959,7 @@ export default class ChatBubbles { } else if(target.classList.contains('forward')) { const mid = +bubble.dataset.mid; const message = await this.managers.appMessagesManager.getMessageByPeer(this.peerId, mid); - new PopupForward({ + PopupElement.createPopup(PopupForward, { [this.peerId]: await this.managers.appMessagesManager.getMidsByMessage(message) }); // appSidebarRight.forwardTab.open([mid]); @@ -3024,6 +3042,16 @@ export default class ChatBubbles { this.chat.input.setStartParam(startParam); } + if(options.mediaTimestamp) { + getHeavyAnimationPromise().then(() => { + this.playMediaWithTimestampAndMid({ + lastMsgId, + middleware, + mediaTimestamp: options.mediaTimestamp + }); + }); + } + return null; } } else { @@ -3251,13 +3279,11 @@ export default class ChatBubbles { pause(400) : Promise.resolve(); p.then(() => { - return this.getMountedBubble(lastMsgId); - }).then((mounted) => { - if(!middleware() || !mounted) { - return; - } - - this.playMediaWithTimestamp(mounted.bubble, options.mediaTimestamp); + return this.playMediaWithTimestampAndMid({ + lastMsgId, + middleware, + mediaTimestamp: options.mediaTimestamp + }); }); } @@ -3305,6 +3331,24 @@ export default class ChatBubbles { return {cached, promise: setPeerPromise}; } + public playMediaWithTimestampAndMid({ + middleware, + lastMsgId, + mediaTimestamp + }: { + middleware: () => boolean, + lastMsgId: number, + mediaTimestamp: number + }) { + this.getMountedBubble(lastMsgId).then((mounted) => { + if(!middleware() || !mounted) { + return; + } + + this.playMediaWithTimestamp(mounted.bubble, mediaTimestamp); + }); + } + public playMediaWithTimestamp(element: HTMLElement, timestamp: number) { const bubble = findUpClassName(element, 'bubble'); const groupedItem = findUpClassName(element, 'grouped-item'); @@ -3325,6 +3369,19 @@ export default class ChatBubbles { audio.playWithTimestamp(timestamp); return; } + + const replyToPeerId = bubble.dataset.replyToPeerId.toPeerId(); + const replyToMid = +bubble.dataset.replyToMid; + if(replyToPeerId && replyToMid) { + if(replyToPeerId === this.peerId) { + this.chat.setMessageId(replyToMid, timestamp); + } else { + this.chat.appImManager.setInnerPeer({ + peerId: replyToPeerId, + mediaTimestamp: timestamp + }); + } + } } private async setFetchReactionsInterval(afterSetPromise: Promise) { @@ -3937,11 +3994,28 @@ export default class ChatBubbles { customEmojiSize ??= this.chat.appImManager.customEmojiSize; - const maxMediaTimestamp = getMediaDurationFromMessage(albumTextMessage || message as Message.message); + let maxMediaTimestamp = getMediaDurationFromMessage(albumTextMessage || message as Message.message); if(albumTextMessage && needToSetHTML) { bubble.dataset.textMid = '' + albumTextMessage.mid; } + if(message.reply_to) { + const replyToPeerId = message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : this.peerId; + bubble.dataset.replyToPeerId = '' + replyToPeerId; + bubble.dataset.replyToMid = '' + message.reply_to_mid; + + if(maxMediaTimestamp === undefined) { + const originalMessage = await rootScope.managers.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid); + if(originalMessage) { + maxMediaTimestamp = getMediaDurationFromMessage(originalMessage as Message.message); + } else { + // this.managers.appMessagesManager.fetchMessageReplyTo(message); + // this.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid}); + maxMediaTimestamp = Infinity; + } + } + } + const richTextOptions: Parameters[1] = { entities: totalEntities, passEntities: this.passEntities, @@ -4095,7 +4169,7 @@ export default class ChatBubbles { } return new Promise((resolve, reject) => { - const popup = new PopupForward(undefined, (peerId) => { + const popup = PopupElement.createPopup(PopupForward, undefined, (peerId) => { resolve(peerId); }); @@ -5726,7 +5800,7 @@ export default class ChatBubbles { if(sponsoredMessage.chat_invite) { callback = () => { - new PopupJoinChatInvite(sponsoredMessage.chat_invite_hash, sponsoredMessage.chat_invite as ChatInvite.chatInvite); + PopupElement.createPopup(PopupJoinChatInvite, sponsoredMessage.chat_invite_hash, sponsoredMessage.chat_invite as ChatInvite.chatInvite); }; } else if(sponsoredMessage.chat_invite_hash) { callback = () => { @@ -6147,6 +6221,11 @@ export default class ChatBubbles { return result; } + // private async getDiscussionMessages() { + // const mids = await this.chat.getMidsByMid(this.chat.threadId); + // return Promise.all(mids.map((mid) => this.chat.getMessage(mid))); + // } + /** * Load and render history * @param maxId max message id diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 176b749c..15e195fa 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -571,11 +571,12 @@ export default class Chat extends EventListenerBase<{ this.autoDownload = await getAutoDownloadSettingsByPeerId(this.peerId); } - public setMessageId(messageId?: number) { + public setMessageId(messageId?: number, mediaTimestamp?: number) { return this.setPeer({ peerId: this.peerId, threadId: this.threadId, - lastMsgId: messageId + lastMsgId: messageId, + mediaTimestamp }); } @@ -711,4 +712,9 @@ export default class Chat extends EventListenerBase<{ public isPinnedMessagesNeeded() { return this.type === 'chat' || this.isForum; } + + public canGiftPremium() { + const peerId = this.peerId; + return peerId.isUser() && this.managers.appProfileManager.canGiftPremium(this.peerId.toUserId()); + } } diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index d07c427d..d56ca3ff 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -48,6 +48,7 @@ import PopupStickers from '../popups/stickers'; import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage'; import canSaveMessageMedia from '../../lib/appManagers/utils/messages/canSaveMessageMedia'; import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText'; +import PopupElement from '../popups'; export default class ChatContextMenu { private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise, notDirect?: () => boolean, withSelection?: true, isSponsored?: true, localName?: 'views' | 'emojis'})[]; @@ -490,7 +491,7 @@ export default class ChatContextMenu { icon: 'flag', text: 'ReportChat', onClick: () => { - new PopupReportMessages(this.peerId, [this.mid]); + PopupElement.createPopup(PopupReportMessages, this.peerId, [this.mid]); }, verify: async() => !this.message.pFlags.out && this.message._ === 'message' && !this.message.pFlags.is_outgoing && await this.managers.appPeersManager.isChannel(this.peerId), notDirect: () => true, @@ -516,7 +517,7 @@ export default class ChatContextMenu { peerId: this.viewerPeerId }); } else if(this.canOpenReactedList) { - new PopupReactedList(this.message as Message.message); + PopupElement.createPopup(PopupReactedList, this.message as Message.message); } else { return false; } @@ -540,7 +541,7 @@ export default class ChatContextMenu { icon: 'info', text: 'Chat.Message.Sponsored.What', onClick: () => { - new PopupSponsored(); + PopupElement.createPopup(PopupSponsored); }, verify: () => false, isSponsored: true @@ -549,7 +550,7 @@ export default class ChatContextMenu { text: 'Loading', onClick: () => { this.emojiInputsPromise.then((inputs) => { - new PopupStickers(inputs, true).show(); + PopupElement.createPopup(PopupStickers, inputs, true).show(); }); }, verify: () => !!this.getUniqueCustomEmojisFromMessage().length, @@ -890,7 +891,7 @@ export default class ChatContextMenu { if(this.chat.selection.isSelecting) { simulateClickEvent(this.chat.selection.selectionSendNowBtn); } else { - new PopupSendNow(this.peerId, await this.chat.getMidsByMid(this.mid)); + PopupElement.createPopup(PopupSendNow, this.peerId, await this.chat.getMidsByMid(this.mid)); } }; @@ -929,11 +930,11 @@ export default class ChatContextMenu { }; private onPinClick = () => { - new PopupPinMessage(this.peerId, this.mid); + PopupElement.createPopup(PopupPinMessage, this.peerId, this.mid); }; private onUnpinClick = () => { - new PopupPinMessage(this.peerId, this.mid, true); + PopupElement.createPopup(PopupPinMessage, this.peerId, this.mid, true); }; private onRetractVote = () => { @@ -968,7 +969,7 @@ export default class ChatContextMenu { if(this.chat.selection.isSelecting) { simulateClickEvent(this.chat.selection.selectionDeleteBtn); } else { - new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid), this.chat.type); + PopupElement.createPopup(PopupDeleteMessages, this.peerId, this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid), this.chat.type); } }; diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index b9a554ac..f3733487 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -111,6 +111,7 @@ import {MARKDOWN_ENTITIES} from '../../lib/richTextProcessor'; import IMAGE_MIME_TYPES_SUPPORTED from '../../environment/imageMimeTypesSupport'; import VIDEO_MIME_TYPES_SUPPORTED from '../../environment/videoMimeTypesSupport'; import {ChatRights} from '../../lib/appManagers/appChatsManager'; +import PopupGiftPremium from '../popups/giftPremium'; const RECORD_MIN_TIME = 500; @@ -644,6 +645,11 @@ export default class ChatInput { this.fileInput.click(); } // verify: () => this.chat.canSend('send_docs') + }, { + icon: 'gift', + text: 'GiftPremium', + onClick: () => this.chat.appImManager.giftPremium(this.chat.peerId), + verify: () => this.chat.canGiftPremium() }, { icon: 'poll', text: 'Poll', @@ -1049,7 +1055,7 @@ export default class ChatInput { this.listenerSetter.add(this.pinnedControlBtn)('click', () => { const peerId = this.chat.peerId; - new PopupPinMessage(peerId, 0, true, () => { + PopupElement.createPopup(PopupPinMessage, peerId, 0, true, () => { this.chat.appImManager.setPeer(); // * close tab // ! костыль, это скроет закреплённые сообщения сразу, вместо того, чтобы ждать пока анимация перехода закончится @@ -1219,7 +1225,7 @@ export default class ChatInput { const middleware = this.chat.bubbles.getMiddleware(); const canSendWhenOnline = rootScope.myId !== peerId && peerId.isUser() && await this.managers.appUsersManager.isUserOnlineVisible(peerId); - new PopupSchedule(initDate, (timestamp) => { + PopupElement.createPopup(PopupSchedule, initDate, (timestamp) => { if(!middleware()) { return; } @@ -2470,7 +2476,7 @@ export default class ChatInput { opusDecodeController.setKeepAlive(true); const showDiscardPopup = () => { - new PopupPeer('popup-cancel-record', { + PopupElement.createPopup(PopupPeer, 'popup-cancel-record', { titleLangKey: 'DiscardVoiceMessageTitle', descriptionLangKey: 'DiscardVoiceMessageDescription', buttons: [{ @@ -2614,7 +2620,7 @@ export default class ChatInput { } if(!draftsAreEqual(draft, originalDraft)) { - new PopupPeer('discard-editing', { + PopupElement.createPopup(PopupPeer, 'discard-editing', { buttons: [{ langKey: 'Alert.Confirm.Discard', callback: () => { @@ -2657,7 +2663,7 @@ export default class ChatInput { this.clearHelper(); this.updateSendBtn(); let selected = false; - const popup = new PopupForward(forwarding, () => { + const popup = PopupElement.createPopup(PopupForward, forwarding, () => { selected = true; }); @@ -2800,7 +2806,7 @@ export default class ChatInput { this.onMessageSent(); } else { - new PopupDeleteMessages(peerId, [editMsgId], chat.type); + PopupElement.createPopup(PopupDeleteMessages, peerId, [editMsgId], chat.type); return; } diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index 1e2bb806..77cdbc86 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -23,6 +23,7 @@ import throttle from '../../helpers/schedulers/throttle'; import {AppManagers} from '../../lib/appManagers/managers'; import {Message} from '../../layer'; import {logger} from '../../lib/logger'; +import PopupElement from '../popups'; class AnimatedSuper { static DURATION = 200; @@ -275,9 +276,9 @@ export default class ChatPinnedMessage { divAndCaption: dAC, onClose: async() => { if(await managers.appPeersManager.canPinMessage(this.chat.peerId)) { - new PopupPinMessage(this.chat.peerId, this.pinnedMid, true); + PopupElement.createPopup(PopupPinMessage, this.chat.peerId, this.pinnedMid, true); } else { - new PopupPinMessage(this.chat.peerId, 0, true); + PopupElement.createPopup(PopupPinMessage, this.chat.peerId, 0, true); } return false; diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index d53d129c..8efcdccb 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -38,6 +38,7 @@ import {attachContextMenuListener} from '../../helpers/dom/attachContextMenuList import filterUnique from '../../helpers/array/filterUnique'; import appImManager from '../../lib/appManagers/appImManager'; import {Message} from '../../layer'; +import PopupElement from '../popups'; const accumulateMapSet = (map: Map>) => { return [...map.values()].reduce((acc, v) => acc + v.size, 0); @@ -676,7 +677,7 @@ export class SearchSelection extends AppSelection { obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b); } - new PopupForward(obj, () => { + PopupElement.createPopup(PopupForward, obj, () => { this.cancelSelection(); }); }, attachClickOptions); @@ -685,7 +686,7 @@ export class SearchSelection extends AppSelection { this.selectionDeleteBtn = ButtonIcon(`delete danger ${BASE_CLASS}-delete`); attachClickEvent(this.selectionDeleteBtn, () => { const peerId = [...this.selectedMids.keys()][0]; - new PopupDeleteMessages(peerId, [...this.selectedMids.get(peerId)], 'chat', () => { + PopupElement.createPopup(PopupDeleteMessages, peerId, [...this.selectedMids.get(peerId)], 'chat', () => { this.cancelSelection(); }); }, attachClickOptions); @@ -949,7 +950,7 @@ export default class ChatSelection extends AppSelection { this.selectionSendNowBtn = Button('btn-primary btn-transparent btn-short text-bold selection-container-send', {icon: 'send2'}); this.selectionSendNowBtn.append(i18n('MessageScheduleSend')); attachClickEvent(this.selectionSendNowBtn, () => { - new PopupSendNow(this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], () => { + PopupElement.createPopup(PopupSendNow, this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], () => { this.cancelSelection(); }); }, attachClickOptions); @@ -962,7 +963,7 @@ export default class ChatSelection extends AppSelection { obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b); } - new PopupForward(obj, () => { + PopupElement.createPopup(PopupForward, obj, () => { this.cancelSelection(); }); }, attachClickOptions); @@ -971,7 +972,7 @@ export default class ChatSelection extends AppSelection { this.selectionDeleteBtn = Button('btn-primary btn-transparent danger text-bold selection-container-delete', {icon: 'delete'}); this.selectionDeleteBtn.append(i18n('Delete')); attachClickEvent(this.selectionDeleteBtn, () => { - new PopupDeleteMessages(this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], this.chat.type, () => { + PopupElement.createPopup(PopupDeleteMessages, this.chat.peerId, [...this.selectedMids.get(this.chat.peerId)], this.chat.type, () => { this.cancelSelection(); }); }, attachClickOptions); diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index 5609f9b0..65c8d334 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -47,6 +47,7 @@ import apiManagerProxy from '../../lib/mtproto/mtprotoworker'; import {makeMediaSize} from '../../helpers/mediaSize'; import {FOLDER_ID_ALL} from '../../lib/mtproto/mtproto_config'; import formatNumber from '../../helpers/number/formatNumber'; +import PopupElement from '../popups'; type ButtonToVerify = {element?: HTMLElement, verify: () => boolean | Promise}; @@ -419,11 +420,11 @@ export default class ChatTopbar { text: 'ShareContact', onClick: () => { const contactPeerId = this.peerId; - new PopupPickUser({ + PopupElement.createPopup(PopupPickUser, { peerTypes: ['dialogs', 'contacts'], onSelect: (peerId) => { return new Promise((resolve, reject) => { - new PopupPeer('', { + PopupElement.createPopup(PopupPeer, '', { titleLangKey: 'SendMessageTitle', descriptionLangKey: 'SendContactToGroupText', descriptionLangArgs: [new PeerTitle({peerId, dialog: true}).element], @@ -453,6 +454,11 @@ export default class ChatTopbar { }); }, verify: async() => rootScope.myId !== this.peerId && this.peerId.isUser() && (await this.managers.appPeersManager.isContact(this.peerId)) && !!(await this.managers.appUsersManager.getUser(this.peerId.toUserId())).phone + }, { + icon: 'gift', + text: 'GiftPremium', + onClick: () => this.chat.appImManager.giftPremium(this.peerId), + verify: () => this.chat.canGiftPremium() }, { icon: 'bots', text: 'Settings', @@ -471,7 +477,7 @@ export default class ChatTopbar { icon: 'lock', text: 'BlockUser', onClick: () => { - new PopupPeer('', { + PopupElement.createPopup(PopupPeer, '', { peerId: this.peerId, titleLangKey: 'BlockUser', descriptionLangKey: 'AreYouSureBlockContact2', @@ -512,7 +518,7 @@ export default class ChatTopbar { icon: 'delete danger', text: 'Delete', onClick: () => { - new PopupDeleteDialog(this.peerId/* , 'leave' */); + PopupElement.createPopup(PopupDeleteDialog, this.peerId/* , 'leave' */); }, verify: async() => this.chat.type === 'chat' && !!(await this.managers.appMessagesManager.getDialogOnly(this.peerId)) }]; @@ -680,7 +686,7 @@ export default class ChatTopbar { } private onMuteClick = () => { - new PopupMute(this.peerId); + PopupElement.createPopup(PopupMute, this.peerId); }; private onResize = () => { diff --git a/src/components/checkboxField.ts b/src/components/checkboxField.ts index bcc1439e..ceabba4b 100644 --- a/src/components/checkboxField.ts +++ b/src/components/checkboxField.ts @@ -26,7 +26,8 @@ export type CheckboxFieldOptions = { restriction?: boolean, withRipple?: boolean, withHover?: boolean, - listenerSetter?: ListenerSetter + listenerSetter?: ListenerSetter, + asRadio?: boolean }; export default class CheckboxField { public input: HTMLInputElement; @@ -54,9 +55,9 @@ export default class CheckboxField { const input = this.input = document.createElement('input'); input.classList.add('checkbox-field-input'); - input.type = 'checkbox'; + input.type = options.asRadio ? 'radio' : 'checkbox'; if(options.name) { - input.id = 'input-' + options.name; + input[options.asRadio ? 'name' : 'id'] = 'input-' + options.name; } if(options.checked) { diff --git a/src/components/confirmationPopup.ts b/src/components/confirmationPopup.ts index f9331485..14ec6dce 100644 --- a/src/components/confirmationPopup.ts +++ b/src/components/confirmationPopup.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import {addCancelButton} from './popups'; +import PopupElement, {addCancelButton} from './popups'; import PopupPeer, {PopupPeerCheckboxOptions, PopupPeerOptions} from './popups/peer'; // type PopupConfirmationOptions = Pick; @@ -35,6 +35,6 @@ export default function confirmationPopup( options.buttons = buttons; options.checkboxes ??= checkbox && [checkbox]; - new PopupPeer('popup-confirmation', options).show(); + PopupElement.createPopup(PopupPeer, 'popup-confirmation', options).show(); }); } diff --git a/src/components/dialogsContextMenu.ts b/src/components/dialogsContextMenu.ts index 2c385a5b..e60eba05 100644 --- a/src/components/dialogsContextMenu.ts +++ b/src/components/dialogsContextMenu.ts @@ -18,6 +18,7 @@ import {AppManagers} from '../lib/appManagers/managers'; import {GENERAL_TOPIC_ID} from '../lib/mtproto/mtproto_config'; import showLimitPopup from './popups/limit'; import createContextMenu from '../helpers/dom/createContextMenu'; +import PopupElement from './popups'; export default class DialogsContextMenu { private buttons: ButtonMenuItemOptionsVerifiable[]; @@ -216,7 +217,7 @@ export default class DialogsContextMenu { }; private onMuteClick = () => { - new PopupMute(this.peerId, this.threadId); + PopupElement.createPopup(PopupMute, this.peerId, this.threadId); }; private onUnreadClick = async() => { @@ -233,6 +234,6 @@ export default class DialogsContextMenu { }; private onDeleteClick = () => { - new PopupDeleteDialog(this.peerId, undefined, undefined, this.threadId); + PopupElement.createPopup(PopupDeleteDialog, this.peerId, undefined, undefined, this.threadId); }; } diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index 3d3b2c25..9a1e7107 100644 --- a/src/components/emoticonsDropdown/tabs/emoji.ts +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -40,6 +40,7 @@ import {hideToast, toastNew} from '../../toast'; import safeAssign from '../../../helpers/object/safeAssign'; import type {AppStickersManager} from '../../../lib/appManagers/appStickersManager'; import liteMode from '../../../helpers/liteMode'; +import PopupElement from '../../popups'; const loadedURLs: Set = new Set(); export function appendEmoji(emoji: string, container?: HTMLElement, prepend = false, unify = false) { @@ -703,7 +704,7 @@ export default class EmojiTab extends EmoticonsTabC { return; } - new PopupStickers({id: category.set.id, access_hash: category.set.access_hash}, true).show(); + PopupElement.createPopup(PopupStickers, {id: category.set.id, access_hash: category.set.access_hash}, true).show(); return; } diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts index f13badda..bfe4cf13 100644 --- a/src/components/emoticonsDropdown/tabs/stickers.ts +++ b/src/components/emoticonsDropdown/tabs/stickers.ts @@ -38,6 +38,7 @@ import {AnyFunction} from '../../../types'; import {IgnoreMouseOutType} from '../../../helpers/dropdownHover'; import customProperties from '../../../helpers/dom/customProperties'; import windowSize from '../../../helpers/windowSize'; +import PopupElement from '../../popups'; export class SuperStickerRenderer { public lazyLoadQueue: LazyLoadQueueRepeat; @@ -617,7 +618,7 @@ export default class StickersTab extends EmoticonsTabC a.months - b.months)[0]; + + const wrapCurrency = (amount: number | string) => paymentsWrapCurrencyAmount(amount, shortestOption.currency, false, true); + + const rows = this.giftOptions.map((giftOption, idx) => { + let subtitle = i18n('PricePerMonth', [wrapCurrency(+giftOption.amount / giftOption.months)]); + if(giftOption !== shortestOption) { + const span = document.createElement('span'); + const badge = document.createElement('span'); + badge.classList.add(className + '-discount'); + const shortestAmount = +shortestOption.amount * giftOption.months / shortestOption.months; + const discount = Math.round((1 - +giftOption.amount / shortestAmount) * 100); + badge.textContent = '-' + discount + '%'; + span.append(badge, subtitle); + subtitle = span; + } + + const isYears = !(giftOption.months % 12); + const checkboxField = new CheckboxField({ + // text: 'Months', + // textArgs: [giftOption.months], + checked: idx === 0, + round: true, + name: 'gift-months', + asRadio: true + }); + + const row = new Row({ + title: i18n(isYears ? 'Years' : 'Months', [isYears ? giftOption.months / 12 : giftOption.months]), + checkboxField, + clickable: true, + subtitle, + titleRightSecondary: wrapCurrency(giftOption.amount) + }); + + row.container.classList.add(className + '-option'); + + return row; + }); + + const form = document.createElement('form'); + form.classList.add(className + '-options'); + form.append(...rows.map((row) => row.container)); + + const buttonText = new I18n.IntlElement({key: 'GiftSubscriptionFor', args: [wrapCurrency(giftOptions[0].amount)]}); + + const getSelectedOption = () => giftOptions[rows.findIndex((row) => row.checkboxField.checked)]; + + this.listenerSetter.add(form)('change', () => { + buttonText.compareAndUpdate({ + args: [ + wrapCurrency(getSelectedOption().amount) + ] + }); + }); + + const giftButton = Button(`btn-primary ${className}-confirm shimmer`); + giftButton.append(buttonText.element); + + attachClickEvent(giftButton, () => { + const giftOption = getSelectedOption(); + appImManager.openUrl(giftOption.bot_url); + this.hide(); + }, {listenerSetter: this.listenerSetter}); + + this.scrollable.append( + avatar, + title, + subtitle, + form, + giftButton + ); + + this.show(); + } +} diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts index 283643f8..05f4f216 100644 --- a/src/components/popups/index.ts +++ b/src/components/popups/index.ts @@ -318,7 +318,7 @@ export default class PopupElement extends return this.POPUPS.filter((element) => element instanceof popupConstructor) as T[]; } - public static createPopup>(ctor: {new(...args: A): T}, ...args: A) { + public static createPopup>(ctor: {new(...args: A): T}, ...args: A) { const popup = new ctor(...args); return popup; } diff --git a/src/components/popups/payment.ts b/src/components/popups/payment.ts index c6460743..cf7fbf91 100644 --- a/src/components/popups/payment.ts +++ b/src/components/popups/payment.ts @@ -531,7 +531,7 @@ export default class PopupPayment extends PopupElement { }; const onMethodClick = () => { - new PopupPaymentCard(paymentForm as PaymentsPaymentForm, previousCardDetails as PaymentCardDetails).addEventListener('finish', ({token, card}) => { + PopupElement.createPopup(PopupPaymentCard, paymentForm as PaymentsPaymentForm, previousCardDetails as PaymentCardDetails).addEventListener('finish', ({token, card}) => { previousToken = token, previousCardDetails = card; setCardSubtitle(card); @@ -587,7 +587,7 @@ export default class PopupPayment extends PopupElement { if(!isReceipt) { onShippingAddressClick = (focus) => { - new PopupPaymentShipping(paymentForm as PaymentsPaymentForm, inputInvoice, focus).addEventListener('finish', ({shippingAddress, requestedInfo}) => { + PopupElement.createPopup(PopupPaymentShipping, paymentForm as PaymentsPaymentForm, inputInvoice, focus).addEventListener('finish', ({shippingAddress, requestedInfo}) => { lastRequestedInfo = requestedInfo; savedInfo = (paymentForm as PaymentsPaymentForm).saved_info = shippingAddress; setShippingInfo(shippingAddress); @@ -643,7 +643,7 @@ export default class PopupPayment extends PopupElement { icon: 'shipping', titleLangKey: 'PaymentCheckoutShippingMethod', clickable: !isReceipt && (onShippingMethodClick = () => { - new PopupPaymentShippingMethods(paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => { + PopupElement.createPopup(PopupPaymentShippingMethods, paymentForm as PaymentsPaymentForm, lastRequestedInfo, lastShippingOption).addEventListener('finish', (shippingOption) => { setShippingOption(shippingOption); }); }) @@ -736,7 +736,7 @@ export default class PopupPayment extends PopupElement { } Promise.resolve(passwordState ?? this.managers.passwordManager.getState()).then((_passwordState) => { - new PopupPaymentCardConfirmation(savedCredentials.title, _passwordState).addEventListener('finish', (tmpPassword) => { + PopupElement.createPopup(PopupPaymentCardConfirmation, savedCredentials.title, _passwordState).addEventListener('finish', (tmpPassword) => { passwordState = undefined; lastTmpPasword = tmpPassword; simulateClickEvent(payButton); @@ -783,7 +783,7 @@ export default class PopupPayment extends PopupElement { if(paymentResult._ === 'payments.paymentResult') { onConfirmed(); } else { - popupPaymentVerification = new PopupPaymentVerification(paymentResult.url, !mediaInvoice.extended_media); + popupPaymentVerification = PopupElement.createPopup(PopupPaymentVerification, paymentResult.url, !mediaInvoice.extended_media); popupPaymentVerification.addEventListener('finish', () => { popupPaymentVerification = undefined; diff --git a/src/components/popups/reportMessages.ts b/src/components/popups/reportMessages.ts index 436212f6..8fac2eb2 100644 --- a/src/components/popups/reportMessages.ts +++ b/src/components/popups/reportMessages.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import PopupElement from '.'; import {attachClickEvent} from '../../helpers/dom/clickEvent'; import findUpClassName from '../../helpers/dom/findUpClassName'; import whichChild from '../../helpers/dom/whichChild'; @@ -45,7 +46,7 @@ export default class PopupReportMessages extends PopupPeer { preloadStickerPromise.then(() => { this.hide(); - new PopupReportMessagesConfirm(peerId, mids, reason, onConfirm); + PopupElement.createPopup(PopupReportMessagesConfirm, peerId, mids, reason, onConfirm); }); }, {listenerSetter: this.listenerSetter}); diff --git a/src/components/popups/unpinMessage.ts b/src/components/popups/unpinMessage.ts index 2f733a39..5d76dbef 100644 --- a/src/components/popups/unpinMessage.ts +++ b/src/components/popups/unpinMessage.ts @@ -110,7 +110,7 @@ export default class PopupPinMessage { addCancelButton(buttons); - const popup = new PopupPeer('popup-delete-chat', { + const popup = PopupElement.createPopup(PopupPeer, 'popup-delete-chat', { peerId, titleLangKey: title, descriptionLangKey: description, diff --git a/src/components/row.ts b/src/components/row.ts index 816932e1..3eeda311 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -33,7 +33,6 @@ export type RowMediaSizeType = 'small' | 'medium' | 'big' | 'abitbigger' | 'bigg export default class Row { public container: HTMLElement; - public title: HTMLElement; public titleRow: HTMLElement; public titleRight: HTMLElement; public media: HTMLElement; @@ -48,6 +47,7 @@ export default class Row { public buttonRight: HTMLElement; + private _title: HTMLElement; private _subtitle: HTMLElement; private _midtitle: HTMLElement; @@ -127,6 +127,9 @@ export default class Row { options.titleRight = this.checkboxField.label; } else { havePadding = true; + if(!this.checkboxField.span) { + this.checkboxField.label.classList.add('checkbox-field-absolute'); + } this.container.append(this.checkboxField.label); } @@ -144,7 +147,7 @@ export default class Row { i.label.classList.add('disable-hover'); } - if(options.title || options.titleLangKey) { + if(options.title || options.titleLangKey || options.titleRight || options.titleRightSecondary) { let c: HTMLElement; const titleRightContent = options.titleRight || options.titleRightSecondary; if(titleRightContent) { @@ -154,7 +157,7 @@ export default class Row { c = this.container; } - this.title = this.createTitle(); + this._title = this.createTitle(); if(options.noWrap) this.title.classList.add('no-wrap'); if(options.title) { setContent(this.title, options.title); @@ -227,6 +230,10 @@ export default class Row { } } + public get title() { + return this._title; + } + public get subtitle() { return this._subtitle ??= this.createSubtitle(); } diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 7cea8180..5f238c30 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -288,6 +288,7 @@ export class AppSidebarLeft extends SidebarSlider { }] }); this.newBtnMenu.className = 'btn-circle rp btn-corner z-depth-1 btn-menu-toggle animated-button-icon'; + this.newBtnMenu.tabIndex = -1; this.newBtnMenu.insertAdjacentHTML('afterbegin', ` @@ -297,6 +298,7 @@ export class AppSidebarLeft extends SidebarSlider { this.updateBtn = document.createElement('div'); this.updateBtn.className = 'btn-circle rp btn-corner z-depth-1 btn-update is-hidden'; + this.updateBtn.tabIndex = -1; ripple(this.updateBtn); this.updateBtn.append(i18n('Update')); diff --git a/src/components/sidebarLeft/tabs/2fa/email.ts b/src/components/sidebarLeft/tabs/2fa/email.ts index f0753007..01f74fa2 100644 --- a/src/components/sidebarLeft/tabs/2fa/email.ts +++ b/src/components/sidebarLeft/tabs/2fa/email.ts @@ -18,6 +18,7 @@ import {attachClickEvent} from '../../../../helpers/dom/clickEvent'; import matchEmail from '../../../../lib/richTextProcessor/matchEmail'; import wrapStickerEmoji from '../../../wrappers/stickerEmoji'; import SettingSection from '../../../settingSection'; +import PopupElement from '../../../popups'; export default class AppTwoStepVerificationEmailTab extends SliderSuperTab { public inputField: InputField; @@ -125,7 +126,7 @@ export default class AppTwoStepVerificationEmailTab extends SliderSuperTab { }; attachClickEvent(btnSkip, (e) => { - const popup = new PopupPeer('popup-skip-email', { + const popup = PopupElement.createPopup(PopupPeer, 'popup-skip-email', { buttons: [{ langKey: 'Cancel', isCancel: true diff --git a/src/components/sidebarLeft/tabs/2fa/index.ts b/src/components/sidebarLeft/tabs/2fa/index.ts index 1cd17ac3..65e5cd1b 100644 --- a/src/components/sidebarLeft/tabs/2fa/index.ts +++ b/src/components/sidebarLeft/tabs/2fa/index.ts @@ -8,6 +8,7 @@ import {attachClickEvent} from '../../../../helpers/dom/clickEvent'; import {AccountPassword} from '../../../../layer'; import {_i18n} from '../../../../lib/langPack'; import Button from '../../../button'; +import PopupElement from '../../../popups'; import PopupPeer from '../../../popups/peer'; import SettingSection from '../../../settingSection'; import {SliderSuperTab} from '../../../slider'; @@ -57,7 +58,7 @@ export default class AppTwoStepVerificationTab extends SliderSuperTab { }); attachClickEvent(btnDisablePassword, () => { - const popup = new PopupPeer('popup-disable-password', { + const popup = PopupElement.createPopup(PopupPeer, 'popup-disable-password', { buttons: [{ langKey: 'Disable', callback: () => { diff --git a/src/components/sidebarLeft/tabs/activeSessions.ts b/src/components/sidebarLeft/tabs/activeSessions.ts index 106f0de7..a43ef7b6 100644 --- a/src/components/sidebarLeft/tabs/activeSessions.ts +++ b/src/components/sidebarLeft/tabs/activeSessions.ts @@ -21,6 +21,7 @@ import {attachContextMenuListener} from '../../../helpers/dom/attachContextMenuL import positionMenu from '../../../helpers/positionMenu'; import contextMenuController from '../../../helpers/contextMenuController'; import SettingSection from '../../settingSection'; +import PopupElement from '../../popups'; export default class AppActiveSessionsTab extends SliderSuperTabEventable { public authorizations: Authorization.authorization[]; @@ -61,7 +62,7 @@ export default class AppActiveSessionsTab extends SliderSuperTabEventable { if(authorizations.length) { const btnTerminate = Button('btn-primary btn-transparent danger', {icon: 'stop', text: 'TerminateAllSessions'}); attachClickEvent(btnTerminate, (e) => { - new PopupPeer('revoke-session', { + PopupElement.createPopup(PopupPeer, 'revoke-session', { buttons: [{ langKey: 'Terminate', isDanger: true, @@ -112,7 +113,7 @@ export default class AppActiveSessionsTab extends SliderSuperTabEventable { const onTerminateClick = () => { const hash = target.dataset.hash; - new PopupPeer('revoke-session', { + PopupElement.createPopup(PopupPeer, 'revoke-session', { buttons: [{ langKey: 'Terminate', isDanger: true, diff --git a/src/components/sidebarLeft/tabs/blockedUsers.ts b/src/components/sidebarLeft/tabs/blockedUsers.ts index 5f38dbff..863c8415 100644 --- a/src/components/sidebarLeft/tabs/blockedUsers.ts +++ b/src/components/sidebarLeft/tabs/blockedUsers.ts @@ -19,6 +19,7 @@ import positionMenu from '../../../helpers/positionMenu'; import contextMenuController from '../../../helpers/contextMenuController'; import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames'; import SettingSection from '../../settingSection'; +import PopupElement from '../../popups'; export default class AppBlockedUsersTab extends SliderSuperTab { public peerIds: PeerId[]; @@ -40,7 +41,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab { this.content.append(btnAdd); attachClickEvent(btnAdd, (e) => { - new PopupPickUser({ + PopupElement.createPopup(PopupPickUser, { peerTypes: ['contacts'], placeholder: 'BlockModal.Search.Placeholder', onSelect: (peerId) => { diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts index 26e7af7a..6942d099 100644 --- a/src/components/sidebarLeft/tabs/editFolder.ts +++ b/src/components/sidebarLeft/tabs/editFolder.ts @@ -24,6 +24,7 @@ import wrapDraftText from '../../../lib/richTextProcessor/wrapDraftText'; import filterAsync from '../../../helpers/array/filterAsync'; import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import SettingSection from '../../settingSection'; +import PopupElement from '../../popups'; const MAX_FOLDER_NAME_LENGTH = 12; @@ -66,7 +67,7 @@ export default class AppEditFolderTab extends SliderSuperTab { icon: 'delete danger', text: 'FilterMenuDelete', onClick: () => { - new PopupPeer('filter-delete', { + PopupElement.createPopup(PopupPeer, 'filter-delete', { titleLangKey: 'ChatList.Filter.Confirm.Remove.Header', descriptionLangKey: 'ChatList.Filter.Confirm.Remove.Text', buttons: [{ diff --git a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts index 04bb68ac..26d793f9 100644 --- a/src/components/sidebarLeft/tabs/privacyAndSecurity.ts +++ b/src/components/sidebarLeft/tabs/privacyAndSecurity.ts @@ -34,6 +34,7 @@ import {toastNew} from '../../toast'; import AppPrivacyVoicesTab from './privacy/voices'; import SettingSection from '../../settingSection'; import AppActiveWebSessionsTab from './activeWebSessions'; +import PopupElement from '../../popups'; export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { private activeSessionsRow: Row; @@ -424,7 +425,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { const section = new SettingSection({name: 'FilterChats'}); const onDeleteClick = () => { - const popup = new PopupPeer('popup-delete-drafts', { + const popup = PopupElement.createPopup(PopupPeer, 'popup-delete-drafts', { buttons: [{ langKey: 'Delete', callback: () => { diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index 33ac51ee..1264add5 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -29,6 +29,7 @@ import PopupElement from '../../popups'; import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import SettingSection from '../../settingSection'; import AppStickersAndEmojiTab from './stickersAndEmoji'; +import ButtonCorner from '../../buttonCorner'; export default class AppSettingsTab extends SliderSuperTab { private buttons: { @@ -58,7 +59,7 @@ export default class AppSettingsTab extends SliderSuperTab { icon: 'logout', text: 'EditAccount.Logout', onClick: () => { - new PopupPeer('logout', { + PopupElement.createPopup(PopupPeer, 'logout', { titleLangKey: 'LogOut', descriptionLangKey: 'LogOut.Description', buttons: [{ @@ -82,7 +83,7 @@ export default class AppSettingsTab extends SliderSuperTab { this.profile.setPeer(rootScope.myId); const fillPromise = this.profile.fillProfileElements(); - const changeAvatarBtn = Button('btn-circle btn-corner z-depth-1 profile-change-avatar', {icon: 'cameraadd'}); + const changeAvatarBtn = ButtonCorner({icon: 'cameraadd', className: 'profile-change-avatar'}); attachClickEvent(changeAvatarBtn, () => { const canvas = document.createElement('canvas'); PopupElement.createPopup(PopupAvatar).open(canvas, (upload) => { diff --git a/src/components/sidebarLeft/tabs/stickersAndEmoji.ts b/src/components/sidebarLeft/tabs/stickersAndEmoji.ts index 64ddb4ee..2ad0a20d 100644 --- a/src/components/sidebarLeft/tabs/stickersAndEmoji.ts +++ b/src/components/sidebarLeft/tabs/stickersAndEmoji.ts @@ -15,6 +15,7 @@ import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText'; import rootScope from '../../../lib/rootScope'; import CheckboxField from '../../checkboxField'; import LazyLoadQueue from '../../lazyLoadQueue'; +import PopupElement from '../../popups'; import PopupStickers from '../../popups/stickers'; import Row, {CreateRowFromCheckboxField} from '../../row'; import SettingSection from '../../settingSection'; @@ -209,7 +210,7 @@ export default class AppStickersAndEmojiTab extends SliderSuperTab { subtitleLangArgs: [stickerSet.count], havePadding: true, clickable: () => { - new PopupStickers({id: stickerSet.id, access_hash: stickerSet.access_hash}).show(); + PopupElement.createPopup(PopupStickers, {id: stickerSet.id, access_hash: stickerSet.access_hash}).show(); }, listenerSetter: this.listenerSetter }); diff --git a/src/components/sidebarRight/tabs/chatType.ts b/src/components/sidebarRight/tabs/chatType.ts index aa3e576f..eccf635f 100644 --- a/src/components/sidebarRight/tabs/chatType.ts +++ b/src/components/sidebarRight/tabs/chatType.ts @@ -27,6 +27,7 @@ import getPeerEditableUsername from '../../../lib/appManagers/utils/peers/getPee import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames'; import {purchaseUsernameCaption} from '../../sidebarLeft/tabs/editProfile'; import confirmationPopup from '../../confirmationPopup'; +import PopupElement from '../../popups'; export default class AppChatTypeTab extends SliderSuperTabEventable { public chatId: ChatId; @@ -102,7 +103,7 @@ export default class AppChatTypeTab extends SliderSuperTabEventable { const btnRevoke = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'RevokeLink'}); attachClickEvent(btnRevoke, () => { - new PopupPeer('revoke-link', { + PopupElement.createPopup(PopupPeer, 'revoke-link', { buttons: [{ langKey: 'RevokeButton', callback: () => { diff --git a/src/components/sidebarRight/tabs/editChat.ts b/src/components/sidebarRight/tabs/editChat.ts index 25022d06..be45b8d4 100644 --- a/src/components/sidebarRight/tabs/editChat.ts +++ b/src/components/sidebarRight/tabs/editChat.ts @@ -24,6 +24,7 @@ import hasRights from '../../../lib/appManagers/utils/chats/hasRights'; import replaceContent from '../../../helpers/dom/replaceContent'; import SettingSection from '../../settingSection'; import getPeerActiveUsernames from '../../../lib/appManagers/utils/peers/getPeerActiveUsernames'; +import PopupElement from '../../popups'; export default class AppEditChatTab extends SliderSuperTab { private chatNameInputField: InputField; @@ -377,7 +378,7 @@ export default class AppEditChatTab extends SliderSuperTab { const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: isBroadcast ? 'PeerInfo.DeleteChannel' : 'DeleteAndExitButton'}); attachClickEvent(btnDelete, () => { - new PopupDeleteDialog(peerId/* , 'delete' */, undefined, (promise) => { + PopupElement.createPopup(PopupDeleteDialog, peerId/* , 'delete' */, undefined, (promise) => { const toggle = toggleDisability([btnDelete], true); promise.then(() => { this.close(); diff --git a/src/components/sidebarRight/tabs/editContact.ts b/src/components/sidebarRight/tabs/editContact.ts index 1b58f685..ffa6aef9 100644 --- a/src/components/sidebarRight/tabs/editContact.ts +++ b/src/components/sidebarRight/tabs/editContact.ts @@ -13,7 +13,7 @@ import Button from '../../button'; import PeerTitle from '../../peerTitle'; import rootScope from '../../../lib/rootScope'; import PopupPeer from '../../popups/peer'; -import {addCancelButton} from '../../popups'; +import PopupElement, {addCancelButton} from '../../popups'; import {i18n} from '../../../lib/langPack'; import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import toggleDisability from '../../../helpers/dom/toggleDisability'; @@ -160,7 +160,7 @@ export default class AppEditContactTab extends SliderSuperTab { const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete', text: 'PeerInfo.DeleteContact'}); attachClickEvent(btnDelete, () => { - new PopupPeer('popup-delete-contact', { + PopupElement.createPopup(PopupPeer, 'popup-delete-contact', { peerId: peerId, titleLangKey: 'DeleteContact', descriptionLangKey: 'AreYouSureDeleteContact', diff --git a/src/components/sidebarRight/tabs/groupPermissions.ts b/src/components/sidebarRight/tabs/groupPermissions.ts index 2ebd0d54..5d8b78a2 100644 --- a/src/components/sidebarRight/tabs/groupPermissions.ts +++ b/src/components/sidebarRight/tabs/groupPermissions.ts @@ -26,6 +26,7 @@ import {SliderSuperTabEventable} from '../../sliderTab'; import {toast} from '../../toast'; import AppUserPermissionsTab from './userPermissions'; import CheckboxFields, {CheckboxFieldsField} from '../../checkboxFields'; +import PopupElement from '../../popups'; type PermissionsCheckboxFieldsField = CheckboxFieldsField & { flags: ChatRights[], @@ -186,7 +187,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable { subtitleLangKey: 'Loading', icon: 'adduser', clickable: () => { - new PopupPickUser({ + PopupElement.createPopup(PopupPickUser, { peerTypes: ['channelParticipants'], onSelect: (peerId) => { setTimeout(() => { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 3e78ddb9..f386baba 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -25,6 +25,7 @@ import {Message} from '../../../layer'; import getMessageThreadId from '../../../lib/appManagers/utils/messages/getMessageThreadId'; import AppEditTopicTab from './editTopic'; import liteMode from '../../../helpers/liteMode'; +import PopupElement from '../../popups'; type SharedMediaHistoryStorage = Partial<{ [type in SearchSuperType]: {mid: number, peerId: PeerId}[] @@ -296,7 +297,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { peerId }).element); - new PopupPeer('popup-add-members', { + PopupElement.createPopup(PopupPeer, 'popup-add-members', { peerId, titleLangKey, descriptionLangKey, @@ -333,7 +334,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { placeholder: 'SendMessageTo' }); } else { - new PopupPickUser({ + PopupElement.createPopup(PopupPickUser, { peerTypes: ['contacts'], placeholder: 'Search', onSelect: (peerId) => { diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts index c24f5569..261c3d5c 100644 --- a/src/components/sidebarRight/tabs/stickers.ts +++ b/src/components/sidebarRight/tabs/stickers.ts @@ -20,6 +20,7 @@ import setInnerHTML from '../../../helpers/dom/setInnerHTML'; import wrapEmojiText from '../../../lib/richTextProcessor/wrapEmojiText'; import attachStickerViewerListeners from '../../stickerViewer'; import wrapSticker from '../../wrappers/sticker'; +import PopupElement from '../../popups'; export default class AppStickersTab extends SliderSuperTab { private inputSearch: InputSearch; @@ -79,7 +80,7 @@ export default class AppStickersTab extends SliderSuperTab { }); } else { this.managers.appStickersManager.getStickerSet({id, access_hash}).then((full) => { - new PopupStickers(full.set).show(); + PopupElement.createPopup(PopupStickers, full.set).show(); }); } }, {listenerSetter: this.listenerSetter}); diff --git a/src/components/topbarCall.ts b/src/components/topbarCall.ts index 6c47743a..4ce28541 100644 --- a/src/components/topbarCall.ts +++ b/src/components/topbarCall.ts @@ -263,14 +263,14 @@ export default class TopbarCall { return; } - new PopupGroupCall().show(); + PopupElement.createPopup(PopupGroupCall).show(); } else if(this.instance instanceof CallInstance) { const popups = PopupElement.getPopups(PopupCall); if(popups.find((popup) => popup.getCallInstance() === this.instance)) { return; } - new PopupCall(this.instance).show(); + PopupElement.createPopup(PopupCall, this.instance).show(); } }, {listenerSetter}); diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index 42e50a31..6234bc62 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -47,6 +47,7 @@ import wrapStickerAnimation from './stickerAnimation'; import framesCache from '../../helpers/framesCache'; import {IS_MOBILE} from '../../environment/userAgent'; import liteMode, {LiteModeKey} from '../../helpers/liteMode'; +import PopupElement from '../popups'; // 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; @@ -699,7 +700,7 @@ function attachStickerEffectHandler({container, doc, managers, middleware, isOut const a = document.createElement('a'); a.onclick = () => { hideToast(); - new PopupStickers(doc.stickerSetInput).show(); + PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show(); }; toastNew({ @@ -782,7 +783,7 @@ export async function onEmojiStickerClick({event, container, managers, peerId, m } const activeAnimations: Set<{}> = (container as any).activeAnimations ??= new Set(); - if(activeAnimations.size >= (IS_MOBILE ? 3 : 5)) { + if(activeAnimations.size >= 3) { return; } diff --git a/src/config/app.ts b/src/config/app.ts index a8784273..92b98154 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: '1.0.1', + langPackVersion: '1.0.3', langPack: 'webk', langPackCode: 'en', domains: MAIN_DOMAINS, diff --git a/src/helpers/addAnchorListener.ts b/src/helpers/addAnchorListener.ts index d917c69c..e9fef34d 100644 --- a/src/helpers/addAnchorListener.ts +++ b/src/helpers/addAnchorListener.ts @@ -22,6 +22,10 @@ export default function addAnchorListener new PopupStickers(doc.stickerSetInput).show(), + onClick: () => PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show(), verify: () => !isStickerPack }, { icon: 'favourites', diff --git a/src/helpers/paymentsWrapCurrencyAmount.ts b/src/helpers/paymentsWrapCurrencyAmount.ts index 24f509c9..4d79542b 100644 --- a/src/helpers/paymentsWrapCurrencyAmount.ts +++ b/src/helpers/paymentsWrapCurrencyAmount.ts @@ -25,7 +25,12 @@ function number_format(number: any, decimals: any, dec_point: any, thousands_sep return s.join(dec); } -export default function paymentsWrapCurrencyAmount(amount: number | string, currency: string, skipSymbol?: boolean) { +export default function paymentsWrapCurrencyAmount( + amount: number | string, + currency: string, + skipSymbol?: boolean, + useNative?: boolean +) { amount = +amount; const isNegative = amount < 0; @@ -47,7 +52,7 @@ export default function paymentsWrapCurrencyAmount(amount: number | string, curr return formatted; } - let symbol = currencyData.symbol; + let symbol = useNative ? currencyData.native || currencyData.symbol : currencyData.symbol; if(isNegative && !currencyData.space_between && currencyData.symbol_left) { symbol = '-' + symbol; formatted = formatted.replace('-', ''); diff --git a/src/lang.ts b/src/lang.ts index 497eb274..1597ed34 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -940,6 +940,11 @@ const lang = { 'SuggestStickersNone': 'None', 'DynamicPackOrder': 'Dynamic Pack Order', 'DynamicPackOrderInfo': 'Automatically place recently used sticker packs at the front of the panel.', + 'GiftPremium': 'Gift Premium', + 'GiftTelegramPremiumTitle': 'Gift Telegram Premium', + 'GiftTelegramPremiumDescription': 'Give **%1$s** access to exclusive features with **Telegram Premium**.', + 'PricePerMonth': '%1$s / month', + 'GiftSubscriptionFor': 'Gift Subscription for %1$s', // * macos 'AccountSettings.Filters': 'Chat Folders', diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 1a0ab5fa..107c659a 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -107,6 +107,7 @@ import partition from '../../helpers/array/partition'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import liteMode, {LiteModeKey} from '../../helpers/liteMode'; import RLottiePlayer from '../rlottie/rlottiePlayer'; +import PopupGiftPremium from '../../components/popups/giftPremium'; export type ChatSavedPosition = { mids: number[], @@ -345,7 +346,7 @@ export class AppImManager extends EventListenerBase<{ const onInstanceDeactivated = (reason: InstanceDeactivateReason) => { const isUpdated = reason === 'version'; - const popup = new PopupElement('popup-instance-deactivated', {overlayClosable: true}); + const popup = PopupElement.createPopup(PopupElement, 'popup-instance-deactivated', {overlayClosable: true}); const c = document.createElement('div'); c.classList.add('instance-deactivated-container'); (popup as any).container.replaceWith(c); @@ -530,7 +531,7 @@ export class AppImManager extends EventListenerBase<{ // return; // } - const popup = new PopupCall(instance); + const popup = PopupElement.createPopup(PopupCall, instance); instance.addEventListener('acceptCallOverride', () => { return this.discardCurrentCall(instance.interlocutorUserId.toPeerId(), undefined, instance) @@ -582,7 +583,7 @@ export class AppImManager extends EventListenerBase<{ a.innerText = href; a.removeAttribute('onclick'); - new PopupPeer('popup-masked-url', { + PopupElement.createPopup(PopupPeer, 'popup-masked-url', { titleLangKey: 'OpenUrlTitle', descriptionLangKey: 'OpenUrlAlert2', descriptionLangArgs: [a], @@ -862,11 +863,11 @@ export class AppImManager extends EventListenerBase<{ const share = apiManagerProxy.share; if(share) { apiManagerProxy.share = undefined; - new PopupForward(undefined, async(peerId) => { + PopupElement.createPopup(PopupForward, undefined, async(peerId) => { await this.setPeer({peerId}); if(share.files?.length) { const foundMedia = share.files.some((file) => MEDIA_MIME_TYPES_SUPPORTED.has(file.type)); - new PopupNewMedia(this.chat, share.files, foundMedia ? 'media' : 'document'); + PopupElement.createPopup(PopupNewMedia, this.chat, share.files, foundMedia ? 'media' : 'document'); } else { this.managers.appMessagesManager.sendText(peerId, share.text); } @@ -1184,7 +1185,7 @@ export class AppImManager extends EventListenerBase<{ case INTERNAL_LINK_TYPE.EMOJI_SET: case INTERNAL_LINK_TYPE.STICKER_SET: { - new PopupStickers({id: link.set}, link._ === INTERNAL_LINK_TYPE.EMOJI_SET).show(); + PopupElement.createPopup(PopupStickers, {id: link.set}, link._ === INTERNAL_LINK_TYPE.EMOJI_SET).show(); break; } @@ -1204,7 +1205,7 @@ export class AppImManager extends EventListenerBase<{ return; } - new PopupJoinChatInvite(link.invite, chatInvite); + PopupElement.createPopup(PopupJoinChatInvite, link.invite, chatInvite); }, (err) => { if(err.type === 'INVITE_HASH_EXPIRED') { toast(i18n('InviteExpired')); @@ -1252,7 +1253,7 @@ export class AppImManager extends EventListenerBase<{ // } // }; - new PopupPayment(undefined, inputInvoice, paymentForm); + PopupElement.createPopup(PopupPayment, undefined, inputInvoice, paymentForm); }); }); break; @@ -2546,6 +2547,12 @@ export class AppImManager extends EventListenerBase<{ options1.threadId === options2.threadId && (typeof(options1.type) !== typeof(options2.type) || options1.type === options2.type); } + + public giftPremium(peerId: PeerId) { + this.managers.appProfileManager.getProfile(peerId.toUserId()).then((profile) => { + PopupElement.createPopup(PopupGiftPremium, peerId, profile.premium_gifts); + }); + } } const appImManager = new AppImManager(); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 978da890..852c1f9d 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -5904,18 +5904,54 @@ export class AppMessagesManager extends AppManager { }); } - public isHistoryResultEnd(historyResult: Exclude, limit: number, add_offset: number) { + public isHistoryResultEnd( + historyResult: Exclude, + limit: number, + add_offset: number, + offset_id: number + ) { const {offset_id_offset, messages} = historyResult as MessagesMessages.messagesMessagesSlice; + const mids = messages.map((message) => { + return (message as MyMessage).mid; + }); + const count = (historyResult as MessagesMessages.messagesMessagesSlice).count || messages.length; - const offsetIdOffset = offset_id_offset ?? count - 1; const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; + const bottomWasMeantToLoad = Math.abs(add_offset); - const isTopEnd = offsetIdOffset >= (count - topWasMeantToLoad) || count < topWasMeantToLoad; - const isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0); + let offsetIdOffset = offset_id_offset; + let isTopEnd = false, isBottomEnd = false; - return {count, offsetIdOffset, isTopEnd, isBottomEnd}; + // if(offsetIdOffset === undefined && !bottomWasMeantToLoad) { + // offsetIdOffset = 0; + // } + + if(offsetIdOffset !== undefined) { + isTopEnd = offsetIdOffset >= (count - topWasMeantToLoad) || count < topWasMeantToLoad; + isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0); + } else if(offset_id && getServerMessageId(offset_id)) { + let i = 0; + for(const length = mids.length; i < length; ++i) { + if(offset_id > mids[i]) { + break; + } + } + + const topLoaded = messages.length - i; + const bottomLoaded = mids.includes(offset_id) ? i - 1 : i; + if(topWasMeantToLoad) isTopEnd = topLoaded < topWasMeantToLoad; + if(bottomWasMeantToLoad) isBottomEnd = bottomLoaded < bottomWasMeantToLoad; + + if(isTopEnd || isBottomEnd) { + offsetIdOffset = isTopEnd ? count - topLoaded : bottomLoaded; + } + } + + offsetIdOffset ??= 0; + + return {count, offsetIdOffset, isTopEnd, isBottomEnd, mids}; } public mergeHistoryResult( @@ -5926,11 +5962,8 @@ export class AppMessagesManager extends AppManager { add_offset: number ) { const {messages} = historyResult as MessagesMessages.messagesMessagesSlice; - const isEnd = this.isHistoryResultEnd(historyResult, limit, add_offset); - const {count, offsetIdOffset, isTopEnd, isBottomEnd} = isEnd; - const mids = messages.map((message) => { - return (message as MyMessage).mid; - }); + const isEnd = this.isHistoryResultEnd(historyResult, limit, add_offset, offset_id); + const {count, offsetIdOffset, isTopEnd, isBottomEnd, mids} = isEnd; // * add bound manually. // * offset_id will be inclusive only if there is 'add_offset' <= -1 (-1 - will only include the 'offset_id') diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index d4daa67b..d5d4a272 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -22,6 +22,7 @@ import {ReferenceContext} from '../mtproto/referenceDatabase'; import generateMessageId from './utils/messageId/generateMessageId'; import assumeType from '../../helpers/assumeType'; import makeError from '../../helpers/makeError'; +import callbackify from '../../helpers/callbackify'; export type UserTyping = Partial<{userId: UserId, action: SendMessageAction, timeout: number}>; @@ -669,6 +670,18 @@ export class AppProfileManager extends AppManager { return this.typingsInPeer[this.getTypingsKey(peerId, threadId)]; } + public canGiftPremium(userId: UserId) { + const user = this.appUsersManager.getUser(userId); + if(user?.pFlags?.premium || true) { + return false; + } + + return callbackify(this.getProfile(userId), (userFull) => { + const user = this.appUsersManager.getUser(userId); + return !!userFull.premium_gifts && !user?.pFlags?.premium; + }); + } + private onUpdateChatParticipants = (update: Update.updateChatParticipants) => { const participants = update.participants; if(participants._ !== 'chatParticipants') { diff --git a/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts b/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts index e786bec9..8914a9ef 100644 --- a/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts +++ b/src/lib/appManagers/utils/messages/getMediaDurationFromMessage.ts @@ -1,6 +1,7 @@ import {Document, Message, MessageMedia} from '../../../../layer'; export default function getMediaDurationFromMessage(message: Message.message) { + if(!message) return undefined; const doc = (message.media as MessageMedia.messageMediaDocument)?.document as Document.document; const duration = ((['voice', 'audio', 'video'] as Document.document['type'][]).includes(doc?.type) && doc.duration) || undefined; return duration; diff --git a/src/lib/richTextProcessor/wrapRichText.ts b/src/lib/richTextProcessor/wrapRichText.ts index 45229224..7f4c3855 100644 --- a/src/lib/richTextProcessor/wrapRichText.ts +++ b/src/lib/richTextProcessor/wrapRichText.ts @@ -1475,6 +1475,10 @@ export default function wrapRichText(text: string, options: Partial<{ (element as HTMLAnchorElement).href = '#'; element.setAttribute('onclick', 'setMediaTimestamp(this)'); + if(options.maxMediaTimestamp === Infinity) { + element.classList.add('is-disabled'); + } + break; } } diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index a8c086b3..5423ca02 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -686,6 +686,31 @@ $btn-menu-z-index: 4; pointer-events: none !important; opacity: var(--disabled-opacity); } + + &.shimmer { + &:before { + content: ""; + position: absolute; + top: 0; + display: block; + width: 100%; + height: 100%; + background: linear-gradient(to right, transparent 0%, rgba(var(--surface-color-rgb), .2) 50%, transparent 100%); + animation: wave 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + + @keyframes wave { + 0% { + transform: translateX(-100%); + } + 50% { + transform: translateX(100%); + } + 100% { + transform: translateX(100%); + } + } + } + } } .btn-control { diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 5143aad8..234be2a4 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -2475,6 +2475,12 @@ $bubble-border-radius-big: 12px; // } } +.timestamp.is-disabled { + color: inherit; + text-decoration: none !important; + cursor: inherit; +} + @keyframes audio-dots { 0% { content: ""; diff --git a/src/scss/partials/_row.scss b/src/scss/partials/_row.scss index f9b4cfa1..9b8aea96 100644 --- a/src/scss/partials/_row.scss +++ b/src/scss/partials/_row.scss @@ -254,6 +254,19 @@ $row-border-radius: $border-radius-medium; margin-inline: .125rem .125rem; padding: 0; } + + &-absolute { + position: absolute; + margin: 0 !important; + padding: 0 !important; + left: 0; + } + + &-round { + .checkbox-box-border { + z-index: unset; + } + } } &-subtitle { diff --git a/src/scss/partials/popups/_giftPremium.scss b/src/scss/partials/popups/_giftPremium.scss new file mode 100644 index 00000000..fe14fbcc --- /dev/null +++ b/src/scss/partials/popups/_giftPremium.scss @@ -0,0 +1,103 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +.popup-gift-premium { + $parent: ".popup"; + + #{$parent} { + &-container { + padding: 0; + width: 26.25rem; + max-width: 26.25rem; + + max-height: unquote('min(100%, 43.5rem)'); + border-radius: $border-radius-huge; + } + + &-header { + height: 3.5rem; + margin: 0; + padding: 0 1rem; + margin-bottom: -2rem; + } + } + + .scrollable-y { + flex: 1 1 auto; + padding: 0 1rem 1rem; + } + + &-avatar { + display: block; + margin: 0 auto; + } + + &-title, + &-subtitle { + text-align: center; + display: block; + } + + &-title { + font-size: var(--font-size-20); + font-weight: var(--font-weight-bold); + margin: .75rem 0; + } + + &-options { + display: flex; + flex-direction: column; + margin: .5rem 0 1rem; + } + + &-option { + margin-top: .5rem; + + &:nth-child(1) { + --primary-color: #C564F3; + } + + &:nth-child(2) { + --primary-color: #AC64F3; + } + + &:nth-child(3) { + --primary-color: #9377FF; + } + } + + &-discount { + background-color: var(--primary-color); + border-radius: 6px; + color: #fff; + margin-right: .375rem; + padding: 0 0.3125rem; + height: 20px; + display: inline-block; + line-height: 20px; + } + + &-confirm { + --ripple-color: rgba(255, 255, 255, #{$hover-alpha}); + background: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%) !important; + font-weight: var(--font-weight-bold); + color: #fff; + text-transform: uppercase; + + @include hover() { + &:after { + content: " "; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #fff; + opacity: $hover-alpha; + } + } + } +} diff --git a/src/scss/partials/popups/_popup.scss b/src/scss/partials/popups/_popup.scss index b92d26ef..5700b7eb 100644 --- a/src/scss/partials/popups/_popup.scss +++ b/src/scss/partials/popups/_popup.scss @@ -119,6 +119,12 @@ .scrollable { position: relative; } + + .scrollable-y-bordered { + &:last-child { + border-bottom: none; + } + } } &-buttons { diff --git a/src/scss/style.scss b/src/scss/style.scss index 6cf0c11c..77a707c9 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -411,6 +411,7 @@ $chat-input-inner-padding-handhelds: .25rem; @import "partials/popups/paymentVerification"; @import "partials/popups/paymentCardConfirmation"; @import "partials/popups/limit"; +@import "partials/popups/giftPremium"; @import "partials/pages/pages"; @import "partials/pages/authCode"; diff --git a/src/scss/tgico/_style.scss b/src/scss/tgico/_style.scss index cf5ac435..132c5fb8 100644 --- a/src/scss/tgico/_style.scss +++ b/src/scss/tgico/_style.scss @@ -3,9 +3,9 @@ @font-face { font-family: '#{$tgico-font-family}'; src: - url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?2fcrrv') format('truetype'), - url('#{$tgico-font-path}/#{$tgico-font-family}.woff?2fcrrv') format('woff'), - url('#{$tgico-font-path}/#{$tgico-font-family}.svg?2fcrrv##{$tgico-font-family}') format('svg'); + url('#{$tgico-font-path}/#{$tgico-font-family}.ttf?bv435t') format('truetype'), + url('#{$tgico-font-path}/#{$tgico-font-family}.woff?bv435t') format('woff'), + url('#{$tgico-font-path}/#{$tgico-font-family}.svg?bv435t##{$tgico-font-family}') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -432,6 +432,11 @@ content: $tgico-gifs; } } +.tgico-gift { + &:before { + content: $tgico-gift; + } +} .tgico-group { &:before { content: $tgico-group; diff --git a/src/scss/tgico/_variables.scss b/src/scss/tgico/_variables.scss index bc53fc65..8d062b12 100644 --- a/src/scss/tgico/_variables.scss +++ b/src/scss/tgico/_variables.scss @@ -78,135 +78,136 @@ $tgico-fullscreen: "\e94c"; $tgico-gc_microphone: "\e94d"; $tgico-gc_microphoneoff: "\e94e"; $tgico-gifs: "\e94f"; -$tgico-group: "\e950"; -$tgico-groupmedia: "\e951"; -$tgico-groupmediaoff: "\e952"; -$tgico-help: "\e953"; -$tgico-hide: "\e954"; -$tgico-image: "\e955"; -$tgico-info: "\e956"; -$tgico-info2: "\e957"; -$tgico-italic: "\e958"; -$tgico-keyboard: "\e959"; -$tgico-lamp: "\e95a"; -$tgico-language: "\e95b"; -$tgico-largepause: "\e95c"; -$tgico-largeplay: "\e95d"; -$tgico-left: "\e95e"; -$tgico-limit_chat: "\e95f"; -$tgico-limit_chats: "\e960"; -$tgico-limit_file: "\e961"; -$tgico-limit_folders: "\e962"; -$tgico-limit_link: "\e963"; -$tgico-limit_pin: "\e964"; -$tgico-link: "\e965"; -$tgico-listscreenshare: "\e966"; -$tgico-livelocation: "\e967"; -$tgico-location: "\e968"; -$tgico-lock: "\e969"; -$tgico-lockoff: "\e96a"; -$tgico-loginlogodesktop: "\e96b"; -$tgico-loginlogomobile: "\e96c"; -$tgico-logout: "\e96d"; -$tgico-mediaspoiler: "\e96e"; -$tgico-mediaspoileroff: "\e96f"; -$tgico-mention: "\e970"; -$tgico-menu: "\e971"; -$tgico-message: "\e972"; -$tgico-messageunread: "\e973"; -$tgico-microphone: "\e974"; -$tgico-microphone_crossed: "\e975"; -$tgico-microphone_crossed_filled: "\e976"; -$tgico-microphone_filled: "\e977"; -$tgico-minus: "\e978"; -$tgico-monospace: "\e979"; -$tgico-more: "\e97a"; -$tgico-mute: "\e97b"; -$tgico-muted: "\e97c"; -$tgico-newchannel: "\e97d"; -$tgico-newchat_filled: "\e97e"; -$tgico-newgroup: "\e97f"; -$tgico-newprivate: "\e980"; -$tgico-next: "\e981"; -$tgico-noncontacts: "\e982"; -$tgico-nosound: "\e983"; -$tgico-passwordoff: "\e984"; -$tgico-pause: "\e985"; -$tgico-permissions: "\e986"; -$tgico-phone: "\e987"; -$tgico-pin: "\e988"; -$tgico-pinlist: "\e989"; -$tgico-pinned_filled: "\e98a"; -$tgico-pinnedchat: "\e98b"; -$tgico-pip: "\e98c"; -$tgico-play: "\e98d"; -$tgico-playback_05: "\e98e"; -$tgico-playback_15: "\e98f"; -$tgico-playback_1x: "\e990"; -$tgico-playback_2x: "\e991"; -$tgico-plus: "\e992"; -$tgico-poll: "\e993"; -$tgico-premium_addone: "\e994"; -$tgico-premium_double: "\e995"; -$tgico-premium_lock: "\e996"; -$tgico-premium_unlock: "\e997"; -$tgico-previous: "\e998"; -$tgico-radiooff: "\e999"; -$tgico-radioon: "\e99a"; -$tgico-reactions: "\e99b"; -$tgico-readchats: "\e99c"; -$tgico-recent: "\e99d"; -$tgico-replace: "\e99e"; -$tgico-reply: "\e99f"; -$tgico-reply_filled: "\e9a0"; -$tgico-rightpanel: "\e9a1"; -$tgico-rotate_left: "\e9a2"; -$tgico-rotate_right: "\e9a3"; -$tgico-saved: "\e9a4"; -$tgico-savedmessages: "\e9a5"; -$tgico-schedule: "\e9a6"; -$tgico-scheduled: "\e9a7"; -$tgico-search: "\e9a8"; -$tgico-select: "\e9a9"; -$tgico-send: "\e9aa"; -$tgico-send2: "\e9ab"; -$tgico-sending: "\e9ac"; -$tgico-sendingerror: "\e9ad"; -$tgico-settings: "\e9ae"; -$tgico-settings_filled: "\e9af"; -$tgico-sharescreen_filled: "\e9b0"; -$tgico-shipping: "\e9b1"; -$tgico-shuffle: "\e9b2"; -$tgico-smallscreen: "\e9b3"; -$tgico-smile: "\e9b4"; -$tgico-spoiler: "\e9b5"; -$tgico-sport: "\e9b6"; -$tgico-star: "\e9b7"; -$tgico-stickers: "\e9b8"; -$tgico-stickers_face: "\e9b9"; -$tgico-stop: "\e9ba"; -$tgico-strikethrough: "\e9bb"; -$tgico-textedit: "\e9bc"; -$tgico-tip: "\e9bd"; -$tgico-tools: "\e9be"; -$tgico-topics: "\e9bf"; -$tgico-transcribe: "\e9c0"; -$tgico-unarchive: "\e9c1"; -$tgico-underline: "\e9c2"; -$tgico-unmute: "\e9c3"; -$tgico-unpin: "\e9c4"; -$tgico-unread: "\e9c5"; -$tgico-up: "\e9c6"; -$tgico-user: "\e9c7"; -$tgico-username: "\e9c8"; -$tgico-videocamera: "\e9c9"; -$tgico-videocamera_crossed_filled: "\e9ca"; -$tgico-videocamera_filled: "\e9cb"; -$tgico-videochat: "\e9cc"; -$tgico-volume_down: "\e9cd"; -$tgico-volume_mute: "\e9ce"; -$tgico-volume_off: "\e9cf"; -$tgico-volume_up: "\e9d0"; -$tgico-zoomin: "\e9d1"; -$tgico-zoomout: "\e9d2"; +$tgico-gift: "\e950"; +$tgico-group: "\e951"; +$tgico-groupmedia: "\e952"; +$tgico-groupmediaoff: "\e953"; +$tgico-help: "\e954"; +$tgico-hide: "\e955"; +$tgico-image: "\e956"; +$tgico-info: "\e957"; +$tgico-info2: "\e958"; +$tgico-italic: "\e959"; +$tgico-keyboard: "\e95a"; +$tgico-lamp: "\e95b"; +$tgico-language: "\e95c"; +$tgico-largepause: "\e95d"; +$tgico-largeplay: "\e95e"; +$tgico-left: "\e95f"; +$tgico-limit_chat: "\e960"; +$tgico-limit_chats: "\e961"; +$tgico-limit_file: "\e962"; +$tgico-limit_folders: "\e963"; +$tgico-limit_link: "\e964"; +$tgico-limit_pin: "\e965"; +$tgico-link: "\e966"; +$tgico-listscreenshare: "\e967"; +$tgico-livelocation: "\e968"; +$tgico-location: "\e969"; +$tgico-lock: "\e96a"; +$tgico-lockoff: "\e96b"; +$tgico-loginlogodesktop: "\e96c"; +$tgico-loginlogomobile: "\e96d"; +$tgico-logout: "\e96e"; +$tgico-mediaspoiler: "\e96f"; +$tgico-mediaspoileroff: "\e970"; +$tgico-mention: "\e971"; +$tgico-menu: "\e972"; +$tgico-message: "\e973"; +$tgico-messageunread: "\e974"; +$tgico-microphone: "\e975"; +$tgico-microphone_crossed: "\e976"; +$tgico-microphone_crossed_filled: "\e977"; +$tgico-microphone_filled: "\e978"; +$tgico-minus: "\e979"; +$tgico-monospace: "\e97a"; +$tgico-more: "\e97b"; +$tgico-mute: "\e97c"; +$tgico-muted: "\e97d"; +$tgico-newchannel: "\e97e"; +$tgico-newchat_filled: "\e97f"; +$tgico-newgroup: "\e980"; +$tgico-newprivate: "\e981"; +$tgico-next: "\e982"; +$tgico-noncontacts: "\e983"; +$tgico-nosound: "\e984"; +$tgico-passwordoff: "\e985"; +$tgico-pause: "\e986"; +$tgico-permissions: "\e987"; +$tgico-phone: "\e988"; +$tgico-pin: "\e989"; +$tgico-pinlist: "\e98a"; +$tgico-pinned_filled: "\e98b"; +$tgico-pinnedchat: "\e98c"; +$tgico-pip: "\e98d"; +$tgico-play: "\e98e"; +$tgico-playback_05: "\e98f"; +$tgico-playback_15: "\e990"; +$tgico-playback_1x: "\e991"; +$tgico-playback_2x: "\e992"; +$tgico-plus: "\e993"; +$tgico-poll: "\e994"; +$tgico-premium_addone: "\e995"; +$tgico-premium_double: "\e996"; +$tgico-premium_lock: "\e997"; +$tgico-premium_unlock: "\e998"; +$tgico-previous: "\e999"; +$tgico-radiooff: "\e99a"; +$tgico-radioon: "\e99b"; +$tgico-reactions: "\e99c"; +$tgico-readchats: "\e99d"; +$tgico-recent: "\e99e"; +$tgico-replace: "\e99f"; +$tgico-reply: "\e9a0"; +$tgico-reply_filled: "\e9a1"; +$tgico-rightpanel: "\e9a2"; +$tgico-rotate_left: "\e9a3"; +$tgico-rotate_right: "\e9a4"; +$tgico-saved: "\e9a5"; +$tgico-savedmessages: "\e9a6"; +$tgico-schedule: "\e9a7"; +$tgico-scheduled: "\e9a8"; +$tgico-search: "\e9a9"; +$tgico-select: "\e9aa"; +$tgico-send: "\e9ab"; +$tgico-send2: "\e9ac"; +$tgico-sending: "\e9ad"; +$tgico-sendingerror: "\e9ae"; +$tgico-settings: "\e9af"; +$tgico-settings_filled: "\e9b0"; +$tgico-sharescreen_filled: "\e9b1"; +$tgico-shipping: "\e9b2"; +$tgico-shuffle: "\e9b3"; +$tgico-smallscreen: "\e9b4"; +$tgico-smile: "\e9b5"; +$tgico-spoiler: "\e9b6"; +$tgico-sport: "\e9b7"; +$tgico-star: "\e9b8"; +$tgico-stickers: "\e9b9"; +$tgico-stickers_face: "\e9ba"; +$tgico-stop: "\e9bb"; +$tgico-strikethrough: "\e9bc"; +$tgico-textedit: "\e9bd"; +$tgico-tip: "\e9be"; +$tgico-tools: "\e9bf"; +$tgico-topics: "\e9c0"; +$tgico-transcribe: "\e9c1"; +$tgico-unarchive: "\e9c2"; +$tgico-underline: "\e9c3"; +$tgico-unmute: "\e9c4"; +$tgico-unpin: "\e9c5"; +$tgico-unread: "\e9c6"; +$tgico-up: "\e9c7"; +$tgico-user: "\e9c8"; +$tgico-username: "\e9c9"; +$tgico-videocamera: "\e9ca"; +$tgico-videocamera_crossed_filled: "\e9cb"; +$tgico-videocamera_filled: "\e9cc"; +$tgico-videochat: "\e9cd"; +$tgico-volume_down: "\e9ce"; +$tgico-volume_mute: "\e9cf"; +$tgico-volume_off: "\e9d0"; +$tgico-volume_up: "\e9d1"; +$tgico-zoomin: "\e9d2"; +$tgico-zoomout: "\e9d3";