diff --git a/src/api/gramjs/apiBuilders/statistics.ts b/src/api/gramjs/apiBuilders/statistics.ts index 9dc6a5643..2419cc4e3 100644 --- a/src/api/gramjs/apiBuilders/statistics.ts +++ b/src/api/gramjs/apiBuilders/statistics.ts @@ -1,11 +1,13 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { + ApiChannelMonetizationStatistics, ApiChannelStatistics, ApiGroupStatistics, ApiMessagePublicForward, ApiPostStatistics, ApiStoryPublicForward, + ChannelMonetizationBalances, PrepaidGiveaway, StatisticsGraph, StatisticsMessageInteractionCounter, StatisticsOverviewItem, @@ -17,6 +19,8 @@ import type { import { buildApiUsernames, buildAvatarPhotoId } from './common'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; +const DECIMALS = 10 ** 9; + export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics { return { // Graphs @@ -49,6 +53,20 @@ export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiC }; } +export function buildChannelMonetizationStatistics( + stats: GramJs.stats.BroadcastRevenueStats, +): ApiChannelMonetizationStatistics { + return { + // Graphs + topHoursGraph: buildGraph(stats.topHoursGraph), + revenueGraph: buildGraph(stats.revenueGraph, undefined, true, stats.usdRate), + + // Statistics overview + balances: buildChannelMonetizationBalances(stats.balances), + usdRate: stats.usdRate, + }; +} + export function buildApiPostInteractionCounter( interaction: GramJs.TypePostInteractionCounters, ): StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter | undefined { @@ -136,7 +154,7 @@ export function buildStoryPublicForwards( } export function buildGraph( - result: GramJs.TypeStatsGraph, isPercentage?: boolean, + result: GramJs.TypeStatsGraph, isPercentage?: boolean, isCurrency?: boolean, currencyRate?: number, ): StatisticsGraph | undefined { if ((result as GramJs.StatsGraphError).error) { return undefined; @@ -156,6 +174,8 @@ export function buildGraph( hasSecondYAxis, isStacked: data.stacked && !hasSecondYAxis, isPercentage, + isCurrency, + currencyRate, datasets: y.map((item: any) => { const key = item[0]; @@ -249,3 +269,15 @@ function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs }, }; } + +function buildChannelMonetizationBalances({ + currentBalance, + availableBalance, + overallRevenue, +}: GramJs.BroadcastRevenueBalances): ChannelMonetizationBalances { + return { + currentBalance: Number(currentBalance) / DECIMALS, + availableBalance: Number(availableBalance) / DECIMALS, + overallRevenue: Number(overallRevenue) / DECIMALS, + }; +} diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index b8f4114f6..4707f1982 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -640,6 +640,7 @@ async function getFullChannelInfo( emojiset, boostsApplied, boostsUnrestrict, + canViewRevenue: canViewMonetization, } = result.fullChat; if (chatPhoto) { @@ -700,6 +701,7 @@ async function getFullChannelInfo( } : undefined, canViewMembers: canViewParticipants, canViewStatistics: canViewStats, + canViewMonetization, isPreHistoryHidden: hiddenPrehistory, members, kickedMembers, diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 93ec6dd24..0a809c562 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -84,6 +84,7 @@ export * from './reactions'; export { fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics, fetchMessagePublicForwards, fetchStatisticsAsyncGraph, fetchStoryStatistics, fetchStoryPublicForwards, + fetchChannelMonetizationStatistics, } from './statistics'; export { diff --git a/src/api/gramjs/methods/statistics.ts b/src/api/gramjs/methods/statistics.ts index 31384c6b7..758a606f5 100644 --- a/src/api/gramjs/methods/statistics.ts +++ b/src/api/gramjs/methods/statistics.ts @@ -8,6 +8,7 @@ import type { import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { + buildChannelMonetizationStatistics, buildChannelStatistics, buildGraph, buildGroupStatistics, @@ -39,6 +40,22 @@ export async function fetchChannelStatistics({ }; } +export async function fetchChannelMonetizationStatistics({ + chat, dcId, +}: { chat: ApiChat; dcId?: number }) { + const result = await invokeRequest(new GramJs.stats.GetBroadcastRevenueStats({ + channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, + }), { + dcId, + }); + + if (!result) { + return undefined; + } + + return buildChannelMonetizationStatistics(result); +} + export async function fetchGroupStatistics({ chat, dcId, }: { chat: ApiChat; dcId?: number }) { diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index e001cc07c..f6889674f 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -126,6 +126,7 @@ export interface ApiChatFullInfo { reactionsLimit?: number; sendAsId?: string; canViewStatistics?: boolean; + canViewMonetization?: boolean; recentRequesterIds?: string[]; requestsPending?: number; statisticsDcId?: number; diff --git a/src/api/types/statistics.ts b/src/api/types/statistics.ts index f91a27adc..4d1a7f19d 100644 --- a/src/api/types/statistics.ts +++ b/src/api/types/statistics.ts @@ -23,6 +23,13 @@ export interface ApiChannelStatistics { recentPosts: Array; } +export interface ApiChannelMonetizationStatistics { + topHoursGraph?: StatisticsGraph | string; + revenueGraph?: StatisticsGraph | string; + balances?: ChannelMonetizationBalances; + usdRate?: number; +} + export interface ApiGroupStatistics { growthGraph?: StatisticsGraph | string; membersGraph?: StatisticsGraph | string; @@ -79,6 +86,8 @@ export interface StatisticsGraph { labels: Array; isStacked: boolean; isPercentage?: boolean; + isCurrency?: boolean; + currencyRate?: number; hideCaption: boolean; hasSecondYAxis: boolean; minimapRange: { @@ -131,3 +140,9 @@ export interface StatisticsStoryInteractionCounter { forwardsCount: number; reactionsCount: number; } + +export interface ChannelMonetizationBalances { + currentBalance: number; + availableBalance: number; + overallRevenue: number; +} diff --git a/src/assets/font-icons/cash-circle.svg b/src/assets/font-icons/cash-circle.svg new file mode 100644 index 000000000..8743da4da --- /dev/null +++ b/src/assets/font-icons/cash-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/font-icons/toncoin.svg b/src/assets/font-icons/toncoin.svg new file mode 100644 index 000000000..2ebd21efa --- /dev/null +++ b/src/assets/font-icons/toncoin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 3350aaac8..211f114d7 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1271,6 +1271,10 @@ "MenuInstallApp" = "Install App"; "RemoveEffect" = "Remove effect"; "ReplyInPrivateMessage" = "Reply In Private Message"; +"MonetizationInfoTONTitle" = "What is 💎 TON?"; +"ChannelEarnLearnCoinAbout" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and low commissions on transactions. {link}"; +"MonetizationBalanceZeroInfo" = "You will be able to collect rewards using Fragment, a third-party platform used by advertisers to pay for ads. {link}"; +"ChannelEarnAbout" = "Telegram shares 50% of the revenue from ads displayed in your channel as rewards. {link}"; "AriaSearchOlderResult" = "Focus next result"; "AriaSearchNewerResult" = "Focus previous result"; "CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}" diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 1695dd434..64f8ed796 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -31,6 +31,7 @@ export { default as StarsBalanceModal } from '../components/modals/stars/StarsBa export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal'; export { default as AboutAdsModal } from '../components/common/AboutAdsModal'; +export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal'; export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal'; export { default as CalendarModal } from '../components/common/CalendarModal'; export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal'; diff --git a/src/components/common/AboutAdsModal.module.scss b/src/components/common/AboutAdsModal.module.scss index a593ec6d8..88ba2f0be 100644 --- a/src/components/common/AboutAdsModal.module.scss +++ b/src/components/common/AboutAdsModal.module.scss @@ -1,3 +1,7 @@ +.root :global(.modal-dialog) { + width: 26.25rem; +} + .title, .description { text-align: center !important; text-wrap: pretty; @@ -8,26 +12,6 @@ color: var(--color-text-secondary); } -.separator { - margin-block: 1rem; - width: 100%; -} - -.topIcon { - --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); - display: grid; - place-items: center; - flex-shrink: 0; - border-radius: 50%; - background: var(--premium-gradient); - - font-size: 4rem; - color: white; - width: 6rem; - height: 6rem; - margin-bottom: 1rem; -} - .content { display: flex; flex-direction: column; diff --git a/src/components/common/AboutAdsModal.tsx b/src/components/common/AboutAdsModal.tsx index d881d3963..5516a45a7 100644 --- a/src/components/common/AboutAdsModal.tsx +++ b/src/components/common/AboutAdsModal.tsx @@ -1,6 +1,8 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo, useMemo } from '../../lib/teact/teact'; +import type { TableAboutData } from '../modals/common/TableAboutModal'; + import buildClassName from '../../util/buildClassName'; import renderText from './helpers/renderText'; @@ -8,27 +10,25 @@ import useSelectorSignal from '../../hooks/data/useSelectorSignal'; import useDerivedState from '../../hooks/useDerivedState'; import useOldLang from '../../hooks/useOldLang'; +import TableAboutModal from '../modals/common/TableAboutModal'; import Button from '../ui/Button'; -import ListItem from '../ui/ListItem'; import Modal from '../ui/Modal'; -import Separator from '../ui/Separator'; -import Icon from './icons/Icon'; import SafeLink from './SafeLink'; import styles from './AboutAdsModal.module.scss'; export type OwnProps = { isOpen: boolean; - isRevenueSharing?: boolean; + isMonetizationSharing?: boolean; onClose: NoneToVoidFunction; }; const AboutAdsModal: FC = ({ isOpen, - isRevenueSharing, + isMonetizationSharing, onClose, }) => { - const lang = useOldLang(); + const oldLang = useOldLang(); const minLevelSignal = useSelectorSignal((global) => global.appConfig?.channelRestrictAdsLevelMin); const minLevelToRestrictAds = useDerivedState(minLevelSignal); @@ -36,84 +36,89 @@ const AboutAdsModal: FC = ({ const regularAdContent = useMemo(() => { return ( <> -

{lang('SponsoredMessageInfoScreen.Title')}

-

{renderText(lang('SponsoredMessageInfoDescription1'), ['br'])}

-

{renderText(lang('SponsoredMessageInfoDescription2'), ['br'])}

-

{renderText(lang('SponsoredMessageInfoDescription3'), ['br'])}

+

{oldLang('SponsoredMessageInfoScreen.Title')}

+

{renderText(oldLang('SponsoredMessageInfoDescription1'), ['br'])}

+

{renderText(oldLang('SponsoredMessageInfoDescription2'), ['br'])}

+

{renderText(oldLang('SponsoredMessageInfoDescription3'), ['br'])}

-

{renderText(lang('SponsoredMessageInfoDescription4'), ['br'])}

+

{renderText(oldLang('SponsoredMessageInfoDescription4'), ['br'])}

); - }, [lang]); + }, [oldLang]); - const revenueSharingAdContent = useMemo(() => { - return ( + const modalData = useMemo(() => { + if (!isOpen) return undefined; + + const header = ( <> -
-

{lang('AboutRevenueSharingAds')}

+

{oldLang('AboutRevenueSharingAds')}

- {lang('RevenueSharingAdsAlertSubtitle')} + {oldLang('RevenueSharingAdsAlertSubtitle')}

- - {lang('RevenueSharingAdsInfo1Title')} - - {renderText(lang('RevenueSharingAdsInfo1Subtitle'), ['simple_markdown'])} - - - - {lang('RevenueSharingAdsInfo2Title')} - - {renderText(lang('RevenueSharingAdsInfo2Subtitle'), ['simple_markdown'])} - - - - {lang('RevenueSharingAdsInfo3Title')} - - {renderText(lang('RevenueSharingAdsInfo3Subtitle', minLevelToRestrictAds), ['simple_markdown'])} - - - -

{renderText(lang('RevenueSharingAdsInfo4Title'), ['simple_markdown'])}

+ + ); + + const listItemData = [ + ['lock', oldLang('RevenueSharingAdsInfo1Title'), + renderText(oldLang('RevenueSharingAdsInfo1Subtitle'), ['simple_markdown'])], + ['revenue-split', oldLang('RevenueSharingAdsInfo2Title'), + renderText(oldLang('RevenueSharingAdsInfo2Subtitle'), ['simple_markdown'])], + ['nochannel', oldLang('RevenueSharingAdsInfo3Title'), + renderText(oldLang('RevenueSharingAdsInfo3Subtitle', minLevelToRestrictAds), ['simple_markdown'])], + ] satisfies TableAboutData; + + const footer = ( + <> +

{renderText(oldLang('RevenueSharingAdsInfo4Title'), ['simple_markdown'])}

- {renderText(lang('RevenueSharingAdsInfo4Subtitle2', ''), ['simple_markdown'])} + {renderText(oldLang('RevenueSharingAdsInfo4Subtitle2', ''), ['simple_markdown'])}

); - }, [lang, minLevelToRestrictAds]); + + return { + header, + listItemData, + footer, + }; + }, [isOpen, oldLang, minLevelToRestrictAds]); + + if (isMonetizationSharing && modalData) { + return ( + + ); + } return ( - {isRevenueSharing ? revenueSharingAdContent : regularAdContent} + {regularAdContent} ); diff --git a/src/components/common/AboutMonetizationModal.async.tsx b/src/components/common/AboutMonetizationModal.async.tsx new file mode 100644 index 000000000..bc48d1400 --- /dev/null +++ b/src/components/common/AboutMonetizationModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../lib/teact/teact'; +import React from '../../lib/teact/teact'; + +import type { OwnProps } from './AboutMonetizationModal'; + +import { Bundles } from '../../util/moduleLoader'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const AboutMonetizationModalAsync: FC = (props) => { + const { isOpen } = props; + const AboutMonetizationModal = useModuleLoader(Bundles.Extra, 'AboutMonetizationModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return AboutMonetizationModal ? : undefined; +}; + +export default AboutMonetizationModalAsync; diff --git a/src/components/common/AboutMonetizationModal.module.scss b/src/components/common/AboutMonetizationModal.module.scss new file mode 100644 index 000000000..01a6d958c --- /dev/null +++ b/src/components/common/AboutMonetizationModal.module.scss @@ -0,0 +1,10 @@ +.title, .description { + text-align: center !important; + text-wrap: pretty; + padding-inline: 1.5rem; +} + +.toncoin { + font-size: 1rem; + color: var(--color-primary); +} diff --git a/src/components/common/AboutMonetizationModal.tsx b/src/components/common/AboutMonetizationModal.tsx new file mode 100644 index 000000000..560fef02b --- /dev/null +++ b/src/components/common/AboutMonetizationModal.tsx @@ -0,0 +1,105 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo, useMemo } from '../../lib/teact/teact'; + +import type { TableAboutData } from '../modals/common/TableAboutModal'; + +import renderText from './helpers/renderText'; + +import useLang from '../../hooks/useLang'; +import useOldLang from '../../hooks/useOldLang'; + +import TableAboutModal from '../modals/common/TableAboutModal'; +import Icon from './icons/Icon'; +import SafeLink from './SafeLink'; + +import styles from './AboutMonetizationModal.module.scss'; + +export type OwnProps = { + isOpen: boolean; + onClose: NoneToVoidFunction; +}; + +const AboutMonetizationModal: FC = ({ + isOpen, + onClose, +}) => { + const oldLang = useOldLang(); + const lang = useLang(); + + const blockchainText = useMemo(() => { + const linkText = oldLang('LearnMore'); + return lang( + 'ChannelEarnLearnCoinAbout', + { + link: ( + + {linkText} + + + ), + }, + { + withNodes: true, + }, + ); + }, [lang, oldLang]); + + const monetizationTitle = useMemo(() => { + return lang( + 'MonetizationInfoTONTitle', + undefined, + { + withNodes: true, + specialReplacement: { '💎': }, + }, + ); + }, [lang]); + + const modalData = useMemo(() => { + if (!isOpen) return undefined; + + const header = ( +

{oldLang('lng_channel_earn_learn_title')}

+ ); + + const listItemData = [ + ['channel', oldLang('lng_channel_earn_learn_in_subtitle'), + renderText(oldLang('lng_channel_earn_learn_in_about'), ['simple_markdown'])], + ['revenue-split', oldLang('lng_channel_earn_learn_split_subtitle'), + renderText(oldLang('Monetization.Intro.Split.Text'), ['simple_markdown'])], + ['cash-circle', oldLang('lng_channel_earn_learn_out_subtitle'), + renderText(oldLang('lng_channel_earn_learn_out_about'), ['simple_markdown'])], + ] satisfies TableAboutData; + + const footer = ( + <> +

{monetizationTitle}

+

{blockchainText}

+ + ); + + return { + header, + listItemData, + footer, + }; + }, [isOpen, oldLang, monetizationTitle, blockchainText]); + + if (!modalData) { + return undefined; + } + + return ( + + ); +}; + +export default memo(AboutMonetizationModal); diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index 31644355e..ed0cddbd3 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -66,6 +66,7 @@ interface StateProps { canCall?: boolean; canMute?: boolean; canViewStatistics?: boolean; + canViewMonetization?: boolean; canViewBoosts?: boolean; canShowBoostModal?: boolean; canLeave?: boolean; @@ -100,6 +101,7 @@ const HeaderActions: FC = ({ canCall, canMute, canViewStatistics, + canViewMonetization, canViewBoosts, canShowBoostModal, canLeave, @@ -433,6 +435,7 @@ const HeaderActions: FC = ({ canMute={canMute} canViewStatistics={canViewStatistics} canViewBoosts={canViewBoosts} + canViewMonetization={canViewMonetization} canShowBoostModal={canShowBoostModal} canLeave={canLeave} canEnterVoiceChat={canEnterVoiceChat} @@ -499,6 +502,7 @@ export default memo(withGlobal( const canCreateVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && !chat.isCallActive && (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat))); const canViewStatistics = isMainThread && chatFullInfo?.canViewStatistics; + const canViewMonetization = isMainThread && chatFullInfo?.canViewMonetization; const canViewBoosts = isMainThread && (isSuperGroup || isChannel) && (canViewStatistics || getHasAdminRight(chat, 'postStories')); const canShowBoostModal = !canViewBoosts && (isSuperGroup || isChannel); @@ -521,6 +525,7 @@ export default memo(withGlobal( canCall, canMute, canViewStatistics, + canViewMonetization, canViewBoosts, canShowBoostModal, canLeave, diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 8618c95ce..77926edb6 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -84,6 +84,7 @@ export type OwnProps = { canMute?: boolean; canViewStatistics?: boolean; canViewBoosts?: boolean; + canViewMonetization?: boolean; canShowBoostModal?: boolean; withForumActions?: boolean; canLeave?: boolean; @@ -145,6 +146,7 @@ const HeaderMenuContainer: FC = ({ canCall, canMute, canViewStatistics, + canViewMonetization, canViewBoosts, pendingJoinRequests, canLeave, @@ -186,6 +188,7 @@ const HeaderMenuContainer: FC = ({ openAddContactDialog, requestMasterAndRequestCall, toggleStatistics, + openMonetizationStatistics, openBoostStatistics, openPremiumGiftModal, openThreadWithInfo, @@ -348,6 +351,12 @@ const HeaderMenuContainer: FC = ({ closeMenu(); }); + const handleMonetizationClick = useLastCallback(() => { + openMonetizationStatistics({ chatId }); + setShouldCloseFast(!isRightColumnShown); + closeMenu(); + }); + const handleBoostClick = useLastCallback(() => { if (canViewBoosts) { openBoostStatistics({ chatId }); @@ -618,6 +627,14 @@ const HeaderMenuContainer: FC = ({ {lang('Statistics')} )} + {isChannel && canViewMonetization && ( + + {lang('lng_channel_earn_title')} + + )} {canTranslate && ( = ({ onShowReactors, onToggleReaction, onCopyMessages, - onAboutAds, + onAboutAdsClick, onSponsoredHide, onSponsorInfo, onSponsoredReport, @@ -461,7 +461,7 @@ const MessageContextMenu: FC = ({ {lang('SponsoredMessageSponsor')} )} {isSponsoredMessage && ( - + {lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')} )} diff --git a/src/components/middle/message/SponsoredMessage.tsx b/src/components/middle/message/SponsoredMessage.tsx index e65af78a8..cd63aedb5 100644 --- a/src/components/middle/message/SponsoredMessage.tsx +++ b/src/components/middle/message/SponsoredMessage.tsx @@ -188,7 +188,7 @@ const SponsoredMessage: FC = ({ isOpen={isContextMenuOpen} anchor={contextMenuPosition} message={message!} - onAboutAds={openAboutAdsModal} + onAboutAdsClick={openAboutAdsModal} onReportAd={handleReportSponsoredMessage} onClose={handleContextMenuClose} onCloseAnimationEnd={handleContextMenuHide} @@ -196,7 +196,7 @@ const SponsoredMessage: FC = ({ )} diff --git a/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx b/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx index 2e808c270..f2ab19255 100644 --- a/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx +++ b/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx @@ -17,7 +17,7 @@ export type OwnProps = { isOpen: boolean; message: ApiSponsoredMessage; anchor: IAnchorPosition; - onAboutAds: NoneToVoidFunction; + onAboutAdsClick: NoneToVoidFunction; onReportAd: NoneToVoidFunction; onClose: NoneToVoidFunction; onCloseAnimationEnd: NoneToVoidFunction; @@ -26,7 +26,7 @@ export type OwnProps = { const SponsoredMessageContextMenuContainer: FC = ({ message, anchor, - onAboutAds, + onAboutAdsClick, onReportAd, onClose, onCloseAnimationEnd, @@ -37,7 +37,7 @@ const SponsoredMessageContextMenuContainer: FC = ({ const { transitionClassNames } = useShowTransition(isMenuOpen, onCloseAnimationEnd, undefined, false); const handleAboutAdsOpen = useLastCallback(() => { - onAboutAds(); + onAboutAdsClick(); closeMenu(); }); @@ -73,7 +73,7 @@ const SponsoredMessageContextMenuContainer: FC = ({ message={message} onClose={closeMenu} onCloseAnimationEnd={closeMenu} - onAboutAds={handleAboutAdsOpen} + onAboutAdsClick={handleAboutAdsOpen} onSponsoredHide={handleSponsoredHide} onSponsorInfo={handleSponsorInfo} onSponsoredReport={handleReportSponsoredMessage} diff --git a/src/components/modals/common/TableAboutModal.module.scss b/src/components/modals/common/TableAboutModal.module.scss new file mode 100644 index 000000000..df6bbad6b --- /dev/null +++ b/src/components/modals/common/TableAboutModal.module.scss @@ -0,0 +1,39 @@ +.root :global(.modal-dialog) { + width: 26.25rem; +} + +.title, .description { + text-align: center !important; + text-wrap: pretty; + padding-inline: 1.5rem; +} + +.secondary { + color: var(--color-text-secondary); +} + +.topIcon { + --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); + display: grid; + place-items: center; + flex-shrink: 0; + border-radius: 50%; + background: var(--premium-gradient); + + font-size: 4rem; + color: white; + width: 6rem; + height: 6rem; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; +} + +.separator { + margin-block: 1rem; + width: 110%; +} diff --git a/src/components/modals/common/TableAboutModal.tsx b/src/components/modals/common/TableAboutModal.tsx new file mode 100644 index 000000000..165fb4382 --- /dev/null +++ b/src/components/modals/common/TableAboutModal.tsx @@ -0,0 +1,68 @@ +import React, { memo, type TeactNode } from '../../../lib/teact/teact'; + +import type { IconName } from '../../../types/icons'; + +import Icon from '../../common/icons/Icon'; +import Button from '../../ui/Button'; +import ListItem from '../../ui/ListItem'; +import Modal from '../../ui/Modal'; +import Separator from '../../ui/Separator'; + +import styles from './TableAboutModal.module.scss'; + +export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][]; + +type OwnProps = { + isOpen?: boolean; + listItemData?: TableAboutData; + headerIconName: IconName; + header?: TeactNode; + footer?: TeactNode; + buttonText?: string; + onClose: NoneToVoidFunction; + onButtonClick?: NoneToVoidFunction; +}; + +const TableAboutModal = ({ + isOpen, + listItemData, + headerIconName, + header, + footer, + buttonText, + onClose, + onButtonClick, +}: OwnProps) => { + return ( + +
+ {header} +
+ {listItemData?.map(([icon, title, subtitle]) => { + return ( + + {title} + {subtitle} + + ); + })} +
+ + {footer} + {buttonText && ( + + )} +
+ ); +}; + +export default memo(TableAboutModal); diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index eb1b3cb93..da1ba39a8 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -35,6 +35,7 @@ import Profile from './Profile'; import RightHeader from './RightHeader'; import BoostStatistics from './statistics/BoostStatistics'; import MessageStatistics from './statistics/MessageStatistics.async'; +import MonetizationStatistics from './statistics/MonetizationStatistics'; import Statistics from './statistics/Statistics.async'; import StoryStatistics from './statistics/StoryStatistics.async'; import StickerSearch from './StickerSearch.async'; @@ -102,6 +103,7 @@ const RightColumn: FC = ({ closeEditTopicPanel, closeBoostStatistics, setShouldCloseRightColumn, + closeMonetizationStatistics, } = getActions(); const { width: windowWidth } = useWindowSize(); @@ -120,6 +122,7 @@ const RightColumn: FC = ({ const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics; const isStoryStatistics = contentKey === RightColumnContent.StoryStatistics; const isBoostStatistics = contentKey === RightColumnContent.BoostStatistics; + const isMonetizationStatistics = contentKey === RightColumnContent.MonetizationStatistics; const isStickerSearch = contentKey === RightColumnContent.StickerSearch; const isGifSearch = contentKey === RightColumnContent.GifSearch; const isPollResults = contentKey === RightColumnContent.PollResults; @@ -197,6 +200,9 @@ const RightColumn: FC = ({ case RightColumnContent.BoostStatistics: closeBoostStatistics(); break; + case RightColumnContent.MonetizationStatistics: + closeMonetizationStatistics(); + break; case RightColumnContent.StickerSearch: blurSearchInput(); setStickerSearchQuery({ query: undefined }); @@ -329,6 +335,8 @@ const RightColumn: FC = ({ return ; case RightColumnContent.BoostStatistics: return ; + case RightColumnContent.MonetizationStatistics: + return ; case RightColumnContent.MessageStatistics: return ; case RightColumnContent.StoryStatistics: @@ -365,6 +373,7 @@ const RightColumn: FC = ({ isManagement={isManagement} isStatistics={isStatistics} isBoostStatistics={isBoostStatistics} + isMonetizationStatistics={isMonetizationStatistics} isMessageStatistics={isMessageStatistics} isStoryStatistics={isStoryStatistics} isStickerSearch={isStickerSearch} diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index abd7947a6..8489fc8fa 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -47,6 +47,7 @@ type OwnProps = { isStatistics?: boolean; isBoostStatistics?: boolean; isMessageStatistics?: boolean; + isMonetizationStatistics?: boolean; isStoryStatistics?: boolean; isStickerSearch?: boolean; isGifSearch?: boolean; @@ -91,6 +92,7 @@ enum HeaderContent { MessageStatistics, StoryStatistics, BoostStatistics, + MonetizationStatistics, Management, ManageInitial, ManageChannelSubscribers, @@ -130,6 +132,7 @@ const RightHeader: FC = ({ isStatistics, isMessageStatistics, isStoryStatistics, + isMonetizationStatistics, isBoostStatistics, isStickerSearch, isGifSearch, @@ -297,6 +300,8 @@ const RightHeader: FC = ({ HeaderContent.CreateTopic ) : isEditingTopic ? ( HeaderContent.EditTopic + ) : isMonetizationStatistics ? ( + HeaderContent.MonetizationStatistics ) : undefined; // When column is closed const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1; @@ -430,6 +435,8 @@ const RightHeader: FC = ({ return

{lang('Stats.StoryTitle')}

; case HeaderContent.BoostStatistics: return

{lang('Boosts')}

; + case HeaderContent.MonetizationStatistics: + return

{lang('lng_channel_earn_title')}

; case HeaderContent.SharedMedia: return

{lang('SharedMedia')}

; case HeaderContent.ManageChannelSubscribers: diff --git a/src/components/right/statistics/MonetizationStatistics.module.scss b/src/components/right/statistics/MonetizationStatistics.module.scss new file mode 100644 index 000000000..1b5dc866d --- /dev/null +++ b/src/components/right/statistics/MonetizationStatistics.module.scss @@ -0,0 +1,76 @@ +@use '../../../styles/mixins'; + +.root { + height: 100%; + overflow-x: hidden; + overflow-y: hidden; +} + +.graph { + margin-bottom: 1rem; + border-bottom: 0.0625rem solid var(--color-borders); + + opacity: 1; + transition: opacity 0.3s ease; + + &:last-of-type { + margin-bottom: 0; + border-bottom: none; + } + + &.hidden { + opacity: 0; + margin: 0; + } +} + +.ready { + overflow-y: scroll !important; +} + +.section { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem 0.75rem; + border-bottom: 0.0625rem solid var(--color-borders); +} + +.topText { + display: block; +} + +.availableReward { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 0.5rem; + line-height: 1.6875rem; +} + +.rewardValue { + font-size: 1.875rem; +} + +.decimalPart { + font-size: 1.375rem; +} + +.integer { + font-size: 1rem; +} + +.decimalUsdPart { + font-size: 0.6875rem; +} + +.toncoinIcon { + font-size: 1.5rem; + margin-inline: 0 0.5rem; + color: var(--color-primary); +} + +.textBottom { + font-size: 0.875rem; + color: var(--color-text-secondary); +} diff --git a/src/components/right/statistics/MonetizationStatistics.tsx b/src/components/right/statistics/MonetizationStatistics.tsx new file mode 100644 index 000000000..502c226b7 --- /dev/null +++ b/src/components/right/statistics/MonetizationStatistics.tsx @@ -0,0 +1,237 @@ +import React, { + memo, useEffect, useMemo, useRef, useState, +} from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { ApiChannelMonetizationStatistics, StatisticsGraph } from '../../../api/types'; + +import { FRAGMENT_ADS_URL } from '../../../config'; +import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; + +import useFlag from '../../../hooks/useFlag'; +import useForceUpdate from '../../../hooks/useForceUpdate'; +import useLang from '../../../hooks/useLang'; +import useOldLang from '../../../hooks/useOldLang'; + +import AboutMonetizationModal from '../../common/AboutMonetizationModal.async'; +import Icon from '../../common/icons/Icon'; +import SafeLink from '../../common/SafeLink'; +import Button from '../../ui/Button'; +import Link from '../../ui/Link'; +import Loading from '../../ui/Loading'; +import StatisticsOverview from './StatisticsOverview'; + +import styles from './MonetizationStatistics.module.scss'; + +type ILovelyChart = { create: Function }; +let lovelyChartPromise: Promise; +let LovelyChart: ILovelyChart; + +async function ensureLovelyChart() { + if (!lovelyChartPromise) { + lovelyChartPromise = import('../../../lib/lovely-chart/LovelyChart') as Promise; + LovelyChart = await lovelyChartPromise; + } + + return lovelyChartPromise; +} + +const MONETIZATION_GRAPHS_TITLES = { + topHoursGraph: 'ChannelStats.Graph.ViewsByHours', + revenueGraph: 'lng_channel_earn_chart_revenue', +}; +const MONETIZATION_GRAPHS = Object.keys(MONETIZATION_GRAPHS_TITLES) as (keyof ApiChannelMonetizationStatistics)[]; + +type StateProps = { + chatId: string; + dcId?: number; + statistics?: ApiChannelMonetizationStatistics; + canCollect?: boolean; +}; + +const MonetizationStatistics = ({ + chatId, + dcId, + statistics, + canCollect, +}: StateProps) => { + const { loadChannelMonetizationStatistics } = getActions(); + const oldLang = useOldLang(); + const lang = useLang(); + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); + const [isReady, setIsReady] = useState(false); + const loadedCharts = useRef([]); + const forceUpdate = useForceUpdate(); + const [isAboutMonetizationModalOpen, openAboutMonetizationModal, closeAboutMonetizationModal] = useFlag(false); + const hasAvailableBalance = Boolean(statistics?.balances?.availableBalance !== 0); + + useEffect(() => { + if (chatId) { + loadChannelMonetizationStatistics({ chatId }); + } + }, [chatId, loadChannelMonetizationStatistics]); + + useEffect(() => { + (async () => { + await ensureLovelyChart(); + + if (!isReady) { + setIsReady(true); + return; + } + + if (!statistics || !containerRef.current) { + return; + } + + MONETIZATION_GRAPHS.forEach((name, index: number) => { + const graph = statistics[name as keyof typeof statistics]; + const isAsync = typeof graph === 'string'; + + if (isAsync || loadedCharts.current.includes(name)) { + return; + } + + if (!graph) { + loadedCharts.current.push(name); + + return; + } + + LovelyChart.create(containerRef.current!.children[index], { + title: oldLang((MONETIZATION_GRAPHS_TITLES as Record)[name]), + ...graph as StatisticsGraph, + }); + + loadedCharts.current.push(name); + + containerRef.current!.children[index].classList.remove(styles.hidden); + }); + + forceUpdate(); + })(); + }, [isReady, statistics, oldLang, chatId, dcId, forceUpdate]); + + function renderAvailableReward() { + const availableBalance = statistics?.balances?.availableBalance; + const [integerTonPart, decimalTonPart] = availableBalance ? availableBalance.toFixed(4).split('.') : [0]; + const [integerUsdPart, decimalUsdPart] = availableBalance + && statistics?.usdRate ? (availableBalance * statistics.usdRate).toFixed(2).split('.') : [0]; + + return ( +
+
+ + + {integerTonPart}.{decimalTonPart} + +
+ {' '} + + ≈ ${integerUsdPart}.{decimalUsdPart} + +
+ ); + } + + const topText = useMemo(() => { + const linkText = oldLang('LearnMore'); + return lang( + 'ChannelEarnAbout', + { + link: ( + + {linkText} + + + ), + }, + { + withNodes: true, + }, + ); + }, [lang, oldLang]); + + const rewardsText = useMemo(() => { + const linkText = oldLang('LearnMore'); + return lang( + 'MonetizationBalanceZeroInfo', + { + link: ( + + {linkText} + + + ), + }, + { + withNodes: true, + }, + ); + }, [lang, oldLang]); + + if (!isReady || !statistics) { + return ; + } + + return ( +
+
{topText}
+ + + + {!loadedCharts.current.length && } + +
+ {MONETIZATION_GRAPHS.map((graph) => ( +
+ ))} +
+ + {hasAvailableBalance && ( +
+ {oldLang('lng_channel_earn_balance_title')} + + {renderAvailableReward()} + + + +
{rewardsText}
+
+ )} + + +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const tabState = selectTabState(global); + const monetizationStatistics = tabState.monetizationStatistics; + const chatId = monetizationStatistics && monetizationStatistics.chatId; + const chat = chatId ? selectChat(global, chatId) : undefined; + const canCollect = chat && chat.isCreator; + const dcId = selectChatFullInfo(global, chatId!)?.statisticsDcId; + const statistics = tabState.statistics.monetization; + + return { + chatId: chatId!, + dcId, + statistics, + canCollect, + }; + }, +)(MonetizationStatistics)); diff --git a/src/components/right/statistics/StatisticsOverview.module.scss b/src/components/right/statistics/StatisticsOverview.module.scss index 4c01f91fb..55076f55b 100644 --- a/src/components/right/statistics/StatisticsOverview.module.scss +++ b/src/components/right/statistics/StatisticsOverview.module.scss @@ -1,7 +1,7 @@ .root { padding: 1rem 0.75rem; margin-bottom: 1rem; - border-bottom: 1px solid var(--color-borders); + border-bottom: 0.0625rem solid var(--color-borders); } .header { @@ -13,10 +13,10 @@ } .title { - margin-right: 2em; - font-size: 16px; + margin-right: 2rem; + font-size: 1rem; color: var(--text-color); - line-height: 30px; + line-height: 1.875rem; text-transform: lowercase; &:first-letter { @@ -61,3 +61,16 @@ color: var(--color-error); } } + +.decimalPart { + font-size: 0.875rem; +} + +.decimalUsdPart { + font-size: 0.6875rem; +} + +.toncoin { + margin-inline: -0.125rem 0.3125rem; + color: var(--color-primary); +} diff --git a/src/components/right/statistics/StatisticsOverview.tsx b/src/components/right/statistics/StatisticsOverview.tsx index a64bd9a5d..72e4c9e06 100644 --- a/src/components/right/statistics/StatisticsOverview.tsx +++ b/src/components/right/statistics/StatisticsOverview.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo } from '../../../lib/teact/teact'; import type { - ApiBoostStatistics, + ApiBoostStatistics, ApiChannelMonetizationStatistics, ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsOverviewItem, } from '../../../api/types'; @@ -12,6 +12,8 @@ import { formatInteger, formatIntegerCompact } from '../../../util/textFormat'; import useOldLang from '../../../hooks/useOldLang'; +import Icon from '../../common/icons/Icon'; + import styles from './StatisticsOverview.module.scss'; type OverviewCell = { @@ -94,19 +96,26 @@ const BOOST_OVERVIEW: OverviewCell[][] = [ ], ]; -type StatisticsType = 'channel' | 'group' | 'message' | 'boost' | 'story'; +type StatisticsType = 'channel' | 'group' | 'message' | 'boost' | 'story' | 'monetization'; export type OwnProps = { type: StatisticsType; title?: string; className?: string; - statistics: ApiChannelStatistics | ApiGroupStatistics | ApiPostStatistics | ApiBoostStatistics; + isToncoin?: boolean; + statistics: + ApiChannelStatistics | + ApiGroupStatistics | + ApiPostStatistics | + ApiBoostStatistics | + ApiChannelMonetizationStatistics; }; const StatisticsOverview: FC = ({ title, type, statistics, + isToncoin, className, }) => { const lang = useOldLang(); @@ -131,7 +140,26 @@ const StatisticsOverview: FC = ({ ); }; + const renderBalanceCell = (balance: number, usdRate: number, text: string) => { + const [integerTonPart, decimalTonPart] = balance.toFixed(4).split('.'); + const [integerUsdPart, decimalUsdPart] = (balance * usdRate).toFixed(2).split('.'); + return ( +
+ + + {integerTonPart}.{decimalTonPart} + + {' '} + + ≈ ${integerUsdPart}.{decimalUsdPart} + +

{lang(text)}

+
+ ); + }; + const { period } = (statistics as ApiGroupStatistics); + const { balances, usdRate } = (statistics as ApiChannelMonetizationStatistics); const schema = getSchemaByType(type); @@ -152,7 +180,15 @@ const StatisticsOverview: FC = ({
- {schema.map((row) => ( + {isToncoin ? ( + + + + ) : schema.map((row) => ( {row.map((cell: OverviewCell) => { const field = (statistics as any)[cell.name]; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 9ff2b3b9e..a630f6e3f 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -179,6 +179,8 @@ const Button: FC = ({ aria-controls={ariaControls} style={style} onTransitionEnd={onTransitionEnd} + target="_blank" + rel="noreferrer" > {children} {!isNotInteractive && ripple && ( diff --git a/src/config.ts b/src/config.ts index 495c39082..600db5662 100644 --- a/src/config.ts +++ b/src/config.ts @@ -319,6 +319,7 @@ export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time'; export const FAQ_URL = 'https://telegram.org/faq'; export const PRIVACY_URL = 'https://telegram.org/privacy'; export const MINI_APP_TOS_URL = 'https://telegram.org/tos/mini-apps'; +export const FRAGMENT_ADS_URL = 'https://fragment.com/ads'; export const GENERAL_TOPIC_ID = 1; export const STORY_EXPIRE_PERIOD = 86400; // 1 day export const STORY_VIEWERS_EXPIRE_PERIOD = 86400; // 1 day diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 7c52b4bda..bca39b66a 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -761,6 +761,20 @@ addActionHandler('openBoostStatistics', async (global, actions, payload): Promis setGlobal(global); }); +addActionHandler('openMonetizationStatistics', (global, actions, payload): ActionReturnType => { + const { chatId, tabId = getCurrentTabId() } = payload; + + const chat = selectChat(global, chatId); + if (!chat) return; + + global = updateTabState(global, { + monetizationStatistics: { + chatId, + }, + }, tabId); + setGlobal(global); +}); + addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise => { const { isGifts, tabId = getCurrentTabId() } = payload || {}; let tabState = selectTabState(global, tabId); diff --git a/src/global/actions/api/statistics.ts b/src/global/actions/api/statistics.ts index 16db2a1b5..da836e7e7 100644 --- a/src/global/actions/api/statistics.ts +++ b/src/global/actions/api/statistics.ts @@ -6,6 +6,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addChats, addUsers, + updateChannelMonetizationStatistics, updateMessageStatistics, updateStatistics, updateStatisticsGraph, @@ -43,6 +44,28 @@ addActionHandler('loadStatistics', async (global, actions, payload): Promise => { + const { + chatId, tabId = getCurrentTabId(), + } = payload; + const chat = selectChat(global, chatId); + const fullInfo = selectChatFullInfo(global, chatId); + if (!chat || !fullInfo) { + return; + } + + const dcId = fullInfo.statisticsDcId; + const stats = await callApi('fetchChannelMonetizationStatistics', { chat, dcId }); + + if (!stats) { + return; + } + + global = getGlobal(); + global = updateChannelMonetizationStatistics(global, stats, tabId); + setGlobal(global); +}); + addActionHandler('loadMessageStatistics', async (global, actions, payload): Promise => { const { chatId, messageId, tabId = getCurrentTabId() } = payload; const chat = selectChat(global, chatId); diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index 6eb9d9931..aaa4c4282 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -430,3 +430,11 @@ addActionHandler('closeBoostStatistics', (global, actions, payload): ActionRetur boostStatistics: undefined, }, tabId); }); + +addActionHandler('closeMonetizationStatistics', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + monetizationStatistics: undefined, + }, tabId); +}); diff --git a/src/global/reducers/statistics.ts b/src/global/reducers/statistics.ts index 284722786..7466ef276 100644 --- a/src/global/reducers/statistics.ts +++ b/src/global/reducers/statistics.ts @@ -1,4 +1,5 @@ import type { + ApiChannelMonetizationStatistics, ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsGraph, } from '../../api/types'; import type { GlobalState, TabArgs } from '../types'; @@ -65,3 +66,15 @@ export function updateStatisticsGraph( }, }, tabId); } + +export function updateChannelMonetizationStatistics( + global: T, statistics: ApiChannelMonetizationStatistics, + ...[tabId = getCurrentTabId()]: TabArgs +): T { + return updateTabState(global, { + statistics: { + ...selectTabState(global, tabId).statistics, + monetization: statistics, + }, + }, tabId); +} diff --git a/src/global/selectors/ui.ts b/src/global/selectors/ui.ts index 022acbfa9..da5c43219 100644 --- a/src/global/selectors/ui.ts +++ b/src/global/selectors/ui.ts @@ -47,6 +47,8 @@ export function selectRightColumnContentKey( RightColumnContent.Statistics ) : tabState.boostStatistics ? ( RightColumnContent.BoostStatistics + ) : tabState.monetizationStatistics ? ( + RightColumnContent.MonetizationStatistics ) : tabState.stickerSearch.query !== undefined ? ( RightColumnContent.StickerSearch ) : tabState.gifSearch.query !== undefined ? ( diff --git a/src/global/types.ts b/src/global/types.ts index fa51cf09b..882d11558 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -6,6 +6,7 @@ import type { ApiAvailableReaction, ApiBoost, ApiBoostsStatus, + ApiChannelMonetizationStatistics, ApiChannelStatistics, ApiChat, ApiChatAdminRights, @@ -603,6 +604,7 @@ export type TabState = { currentMessageId?: number; currentStory?: ApiPostStatistics; currentStoryId?: number; + monetization?: ApiChannelMonetizationStatistics; }; newContact?: { @@ -790,6 +792,10 @@ export type TabState = { }; }; + monetizationStatistics?: { + chatId: string; + }; + giftCodeModal?: { slug: string; message?: { @@ -1800,6 +1806,9 @@ export interface ActionPayloads { name: string; isPercentage?: boolean; } & WithTabId; + loadChannelMonetizationStatistics: { + chatId: string; + } & WithTabId; // ui dismissDialog: WithTabId | undefined; @@ -2533,6 +2542,11 @@ export interface ActionPayloads { chatId: string; } & WithTabId; + openMonetizationStatistics: { + chatId: string; + } & WithTabId; + closeMonetizationStatistics: WithTabId | undefined; + // Media Viewer & Audio Player openMediaViewer: { chatId?: string; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index a2f2cbe5d..8bcb08ea7 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1665,6 +1665,7 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; +stats.getBroadcastRevenueStats#75dfb671 flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastRevenueStats; chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector = chatlists.ExportedChatlistInvite; chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector = ExportedChatlistInvite; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index da987f947..dc7766592 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -271,6 +271,7 @@ "help.getPeerColors", "help.getTimezonesList", "stats.getBroadcastStats", + "stats.getBroadcastRevenueStats", "stats.getMegagroupStats", "stats.getMessagePublicForwards", "stats.getMessageStats", diff --git a/src/lib/lovely-chart/Axes.js b/src/lib/lovely-chart/Axes.js index c0d354b41..07d4cdda0 100644 --- a/src/lib/lovely-chart/Axes.js +++ b/src/lib/lovely-chart/Axes.js @@ -1,5 +1,5 @@ import { GUTTER, AXES_FONT, X_AXIS_HEIGHT, X_AXIS_SHIFT_START, PLOT_TOP_PADDING } from './constants'; -import { humanize } from './format'; +import { formatCryptoValue, humanize } from './format'; import { getCssColor } from './skin'; import { applyXEdgeOpacity, applyYEdgeOpacity, xScaleLevelToStep, yScaleLevelToStep } from './formulas'; import { toPixels } from './Projection'; @@ -47,6 +47,8 @@ export function createAxes(context, data, plotSize, colors) { if (data.isPercentage) { _drawYAxisPercents(projection); + } else if (data.isCurrency) { + _drawYAxisCurrency(projection, data); } else { _drawYAxisScaled( state, @@ -169,5 +171,49 @@ export function createAxes(context, data, plotSize, colors) { context.stroke(); } + function _drawYAxisCurrency(projection, data) { + const formatValue = data.datasets[0].values.map(value => formatCryptoValue(value)); + + const total = formatValue.reduce((sum, value) => sum + value, 0); + const avg1 = total / formatValue.length; + const avg2 = total / (formatValue.length / 2); + const avg3 = total / (formatValue.length / 3); + + const averageRate1 = avg1 * data.currencyRate; + const averageRate2 = avg2 * data.currencyRate; + const averageRate3 = avg3 * data.currencyRate; + + const totalAvg = [0, avg1, avg2, avg3]; + const totalRate = [0, averageRate1, averageRate2, averageRate3]; + + const [, height] = projection.getSize(); + + context.font = AXES_FONT; + context.textAlign = 'left'; + context.textBaseline = 'bottom'; + context.lineWidth = 1; + + context.beginPath(); + + totalAvg.forEach((value, index) => { + const yPx = height - height * (value / Math.max(...formatValue)) + PLOT_TOP_PADDING; + + context.fillStyle = getCssColor(colors, 'y-axis-text', 1); + + context.fillText(`${value.toFixed(2)} TON`, GUTTER, yPx - GUTTER / 4); + + context.textAlign = 'right'; + context.fillText(`$${totalRate[index].toFixed(2)}`, plotSize.width - GUTTER, yPx - GUTTER / 4); + + context.textAlign = 'left'; + + context.moveTo(GUTTER, yPx); + context.strokeStyle = getCssColor(colors, 'grid-lines', 1); + context.lineTo(plotSize.width - GUTTER, yPx); + }); + + context.stroke(); + } + return { drawXAxis, drawYAxis }; } diff --git a/src/lib/lovely-chart/Tooltip.js b/src/lib/lovely-chart/Tooltip.js index 5cc774daf..55886b0d5 100644 --- a/src/lib/lovely-chart/Tooltip.js +++ b/src/lib/lovely-chart/Tooltip.js @@ -1,7 +1,7 @@ import { setupCanvas, clearCanvas } from './canvas'; import { BALLOON_OFFSET, X_AXIS_HEIGHT } from './constants'; import { getPieRadius } from './formulas'; -import { formatInteger, getLabelDate, getLabelTime, statsFormatDayHourFull } from './format'; +import {formatCryptoValue, formatInteger, getLabelDate, getLabelTime, statsFormatDayHourFull} from './format'; import { getCssColor } from './skin'; import { throttle, throttleWithRaf } from './utils'; import { addEventListener, createElement } from './minifiers'; @@ -353,7 +353,12 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus currentDataSet.setAttribute('data-present', 'true'); const valueElement = currentDataSet.querySelector(`.lovely-chart--tooltip-dataset-value.lovely-chart--color-${data.colors[key].slice(1)}:not(.lovely-chart--state-hidden)`); - valueElement.innerHTML = formatInteger(value); + + if (data.isCurrency) { + valueElement.innerHTML = formatCryptoValue(value); + } else { + valueElement.innerHTML = formatInteger(value); + } _renderPercentageValue(currentDataSet, value, totalValue); } @@ -413,6 +418,10 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus _renderTotal(dataSetContainer, formatInteger(totalValue)); } + if (data.isCurrency) { + _renderCurrencyRate(dataSetContainer, formatCryptoValue(totalValue)); + } + Array.from(dataSetContainer.querySelectorAll('[data-present="false"]')) .forEach((dataSet) => { dataSet.remove(); @@ -442,6 +451,28 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus } } + function _renderCurrencyRate(dataSetContainer, totalValue) { + const totalText = dataSetContainer.querySelector(`[data-total="true"]`); + const className = `lovely-chart--tooltip-dataset-value lovely-chart--position-right`; + + const totalUsd = (parseFloat(totalValue) * data.currencyRate).toFixed(2); + + if (!totalText) { + const newTotalText = createElement(); + newTotalText.className = 'lovely-chart--tooltip-dataset'; + newTotalText.setAttribute('data-present', 'true'); + newTotalText.setAttribute('data-total', 'true'); + newTotalText.innerHTML = `USD ≈$${totalUsd}`; + dataSetContainer.appendChild(newTotalText); + } else { + totalText.setAttribute('data-present', 'true'); + + const valueElement = totalText.querySelector(`.lovely-chart--tooltip-dataset-value:not(.lovely-chart--state-hidden)`); + valueElement.innerHTML = `$${totalUsd}`; + } + } + + function _hideBalloon() { _balloon.classList.remove('lovely-chart--state-shown'); } diff --git a/src/lib/lovely-chart/data.js b/src/lib/lovely-chart/data.js index 2e07a4c40..d61dc9ea2 100644 --- a/src/lib/lovely-chart/data.js +++ b/src/lib/lovely-chart/data.js @@ -1,8 +1,8 @@ import { getMaxMin } from './utils'; -import { statsFormatHour, statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format'; +import { statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format'; export function analyzeData(data) { - const { title, labelFormatter, tooltipFormatter, isStacked, isPercentage, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel } = data; + const { title, labelFormatter, tooltipFormatter, isStacked, isPercentage, isCurrency, currencyRate, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel } = data; const { datasets, labels } = prepareDatasets(data); const colors = {}; @@ -20,8 +20,13 @@ export function analyzeData(data) { } }); + let effectiveLabelFormatter = labelFormatter; + if (isCurrency) { + effectiveLabelFormatter = 'statsFormat(\'day\')'; + } + let xLabels; - switch (labelFormatter) { + switch (effectiveLabelFormatter) { case 'statsFormatDayHour': xLabels = statsFormatDayHour(labels); break; @@ -45,6 +50,8 @@ export function analyzeData(data) { datasets, isStacked, isPercentage, + isCurrency, + currencyRate, hasSecondYAxis, onZoom, isLines: data.type === 'line', diff --git a/src/lib/lovely-chart/format.js b/src/lib/lovely-chart/format.js index e74a4d099..d0b9611a5 100644 --- a/src/lib/lovely-chart/format.js +++ b/src/lib/lovely-chart/format.js @@ -62,6 +62,10 @@ export function formatInteger(n) { return String(n).replace(/\d(?=(\d{3})+$)/g, '$& '); } +export function formatCryptoValue(n) { + return Number(n / 10 ** 9); +} + export function getFullLabelDate(label, { isShort = false } = {}) { return getLabelDate(label, { isShort, displayWeekDay: true }); } diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 43154a8b3..2db14044e 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -67,208 +67,210 @@ $icons-map: ( "camera": "\f124", "car": "\f125", "card": "\f126", - "channel-filled": "\f127", - "channel": "\f128", - "channelviews": "\f129", - "chat-badge": "\f12a", - "chats-badge": "\f12b", - "check": "\f12c", - "clock": "\f12d", - "close-circle": "\f12e", - "close-topic": "\f12f", - "close": "\f130", - "cloud-download": "\f131", - "collapse": "\f132", - "colorize": "\f133", - "comments-sticker": "\f134", - "comments": "\f135", - "copy-media": "\f136", - "copy": "\f137", - "darkmode": "\f138", - "data": "\f139", - "delete-filled": "\f13a", - "delete-left": "\f13b", - "delete-user": "\f13c", - "delete": "\f13d", - "document": "\f13e", - "double-badge": "\f13f", - "down": "\f140", - "download": "\f141", - "eats": "\f142", - "edit": "\f143", - "email": "\f144", - "enter": "\f145", - "expand": "\f146", - "eye-closed-outline": "\f147", - "eye-closed": "\f148", - "eye-outline": "\f149", - "eye": "\f14a", - "favorite-filled": "\f14b", - "favorite": "\f14c", - "file-badge": "\f14d", - "flag": "\f14e", - "folder-badge": "\f14f", - "folder": "\f150", - "fontsize": "\f151", - "forums": "\f152", - "forward": "\f153", - "fullscreen": "\f154", - "gifs": "\f155", - "gift": "\f156", - "group-filled": "\f157", - "group": "\f158", - "grouped-disable": "\f159", - "grouped": "\f15a", - "hand-stop": "\f15b", - "hashtag": "\f15c", - "heart-outline": "\f15d", - "heart": "\f15e", - "help": "\f15f", - "info-filled": "\f160", - "info": "\f161", - "install": "\f162", - "italic": "\f163", - "key": "\f164", - "keyboard": "\f165", - "lamp": "\f166", - "language": "\f167", - "large-pause": "\f168", - "large-play": "\f169", - "link-badge": "\f16a", - "link-broken": "\f16b", - "link": "\f16c", - "location": "\f16d", - "lock-badge": "\f16e", - "lock": "\f16f", - "logout": "\f170", - "loop": "\f171", - "mention": "\f172", - "message-failed": "\f173", - "message-pending": "\f174", - "message-read": "\f175", - "message-succeeded": "\f176", - "message": "\f177", - "microphone-alt": "\f178", - "microphone": "\f179", - "monospace": "\f17a", - "more-circle": "\f17b", - "more": "\f17c", - "move-caption-down": "\f17d", - "move-caption-up": "\f17e", - "mute": "\f17f", - "muted": "\f180", - "my-notes": "\f181", - "new-chat-filled": "\f182", - "next": "\f183", - "nochannel": "\f184", - "noise-suppression": "\f185", - "non-contacts": "\f186", - "one-filled": "\f187", - "open-in-new-tab": "\f188", - "password-off": "\f189", - "pause": "\f18a", - "permissions": "\f18b", - "phone-discard-outline": "\f18c", - "phone-discard": "\f18d", - "phone": "\f18e", - "photo": "\f18f", - "pin-badge": "\f190", - "pin-list": "\f191", - "pin": "\f192", - "pinned-chat": "\f193", - "pinned-message": "\f194", - "pip": "\f195", - "play-story": "\f196", - "play": "\f197", - "poll": "\f198", - "previous": "\f199", - "privacy-policy": "\f19a", - "quote-text": "\f19b", - "quote": "\f19c", - "readchats": "\f19d", - "recent": "\f19e", - "reload": "\f19f", - "remove-quote": "\f1a0", - "remove": "\f1a1", - "reopen-topic": "\f1a2", - "replace": "\f1a3", - "replies": "\f1a4", - "reply-filled": "\f1a5", - "reply": "\f1a6", - "revenue-split": "\f1a7", - "revote": "\f1a8", - "save-story": "\f1a9", - "saved-messages": "\f1aa", - "schedule": "\f1ab", - "search": "\f1ac", - "select": "\f1ad", - "send-outline": "\f1ae", - "send": "\f1af", - "settings-filled": "\f1b0", - "settings": "\f1b1", - "share-filled": "\f1b2", - "share-screen-outlined": "\f1b3", - "share-screen-stop": "\f1b4", - "share-screen": "\f1b5", - "show-message": "\f1b6", - "sidebar": "\f1b7", - "skip-next": "\f1b8", - "skip-previous": "\f1b9", - "smallscreen": "\f1ba", - "smile": "\f1bb", - "sort": "\f1bc", - "speaker-muted-story": "\f1bd", - "speaker-outline": "\f1be", - "speaker-story": "\f1bf", - "speaker": "\f1c0", - "spoiler-disable": "\f1c1", - "spoiler": "\f1c2", - "sport": "\f1c3", - "star": "\f1c4", - "stars-lock": "\f1c5", - "stats": "\f1c6", - "stealth-future": "\f1c7", - "stealth-past": "\f1c8", - "stickers": "\f1c9", - "stop-raising-hand": "\f1ca", - "stop": "\f1cb", - "story-caption": "\f1cc", - "story-expired": "\f1cd", - "story-priority": "\f1ce", - "story-reply": "\f1cf", - "strikethrough": "\f1d0", - "tag-add": "\f1d1", - "tag-crossed": "\f1d2", - "tag-filter": "\f1d3", - "tag-name": "\f1d4", - "tag": "\f1d5", - "timer": "\f1d6", - "transcribe": "\f1d7", - "truck": "\f1d8", - "unarchive": "\f1d9", - "underlined": "\f1da", - "unlock-badge": "\f1db", - "unlock": "\f1dc", - "unmute": "\f1dd", - "unpin": "\f1de", - "unread": "\f1df", - "up": "\f1e0", - "user-filled": "\f1e1", - "user-online": "\f1e2", - "user": "\f1e3", - "video-outlined": "\f1e4", - "video-stop": "\f1e5", - "video": "\f1e6", - "view-once": "\f1e7", - "voice-chat": "\f1e8", - "volume-1": "\f1e9", - "volume-2": "\f1ea", - "volume-3": "\f1eb", - "web": "\f1ec", - "webapp": "\f1ed", - "word-wrap": "\f1ee", - "zoom-in": "\f1ef", - "zoom-out": "\f1f0", + "cash-circle": "\f127", + "channel-filled": "\f128", + "channel": "\f129", + "channelviews": "\f12a", + "chat-badge": "\f12b", + "chats-badge": "\f12c", + "check": "\f12d", + "clock": "\f12e", + "close-circle": "\f12f", + "close-topic": "\f130", + "close": "\f131", + "cloud-download": "\f132", + "collapse": "\f133", + "colorize": "\f134", + "comments-sticker": "\f135", + "comments": "\f136", + "copy-media": "\f137", + "copy": "\f138", + "darkmode": "\f139", + "data": "\f13a", + "delete-filled": "\f13b", + "delete-left": "\f13c", + "delete-user": "\f13d", + "delete": "\f13e", + "document": "\f13f", + "double-badge": "\f140", + "down": "\f141", + "download": "\f142", + "eats": "\f143", + "edit": "\f144", + "email": "\f145", + "enter": "\f146", + "expand": "\f147", + "eye-closed-outline": "\f148", + "eye-closed": "\f149", + "eye-outline": "\f14a", + "eye": "\f14b", + "favorite-filled": "\f14c", + "favorite": "\f14d", + "file-badge": "\f14e", + "flag": "\f14f", + "folder-badge": "\f150", + "folder": "\f151", + "fontsize": "\f152", + "forums": "\f153", + "forward": "\f154", + "fullscreen": "\f155", + "gifs": "\f156", + "gift": "\f157", + "group-filled": "\f158", + "group": "\f159", + "grouped-disable": "\f15a", + "grouped": "\f15b", + "hand-stop": "\f15c", + "hashtag": "\f15d", + "heart-outline": "\f15e", + "heart": "\f15f", + "help": "\f160", + "info-filled": "\f161", + "info": "\f162", + "install": "\f163", + "italic": "\f164", + "key": "\f165", + "keyboard": "\f166", + "lamp": "\f167", + "language": "\f168", + "large-pause": "\f169", + "large-play": "\f16a", + "link-badge": "\f16b", + "link-broken": "\f16c", + "link": "\f16d", + "location": "\f16e", + "lock-badge": "\f16f", + "lock": "\f170", + "logout": "\f171", + "loop": "\f172", + "mention": "\f173", + "message-failed": "\f174", + "message-pending": "\f175", + "message-read": "\f176", + "message-succeeded": "\f177", + "message": "\f178", + "microphone-alt": "\f179", + "microphone": "\f17a", + "monospace": "\f17b", + "more-circle": "\f17c", + "more": "\f17d", + "move-caption-down": "\f17e", + "move-caption-up": "\f17f", + "mute": "\f180", + "muted": "\f181", + "my-notes": "\f182", + "new-chat-filled": "\f183", + "next": "\f184", + "nochannel": "\f185", + "noise-suppression": "\f186", + "non-contacts": "\f187", + "one-filled": "\f188", + "open-in-new-tab": "\f189", + "password-off": "\f18a", + "pause": "\f18b", + "permissions": "\f18c", + "phone-discard-outline": "\f18d", + "phone-discard": "\f18e", + "phone": "\f18f", + "photo": "\f190", + "pin-badge": "\f191", + "pin-list": "\f192", + "pin": "\f193", + "pinned-chat": "\f194", + "pinned-message": "\f195", + "pip": "\f196", + "play-story": "\f197", + "play": "\f198", + "poll": "\f199", + "previous": "\f19a", + "privacy-policy": "\f19b", + "quote-text": "\f19c", + "quote": "\f19d", + "readchats": "\f19e", + "recent": "\f19f", + "reload": "\f1a0", + "remove-quote": "\f1a1", + "remove": "\f1a2", + "reopen-topic": "\f1a3", + "replace": "\f1a4", + "replies": "\f1a5", + "reply-filled": "\f1a6", + "reply": "\f1a7", + "revenue-split": "\f1a8", + "revote": "\f1a9", + "save-story": "\f1aa", + "saved-messages": "\f1ab", + "schedule": "\f1ac", + "search": "\f1ad", + "select": "\f1ae", + "send-outline": "\f1af", + "send": "\f1b0", + "settings-filled": "\f1b1", + "settings": "\f1b2", + "share-filled": "\f1b3", + "share-screen-outlined": "\f1b4", + "share-screen-stop": "\f1b5", + "share-screen": "\f1b6", + "show-message": "\f1b7", + "sidebar": "\f1b8", + "skip-next": "\f1b9", + "skip-previous": "\f1ba", + "smallscreen": "\f1bb", + "smile": "\f1bc", + "sort": "\f1bd", + "speaker-muted-story": "\f1be", + "speaker-outline": "\f1bf", + "speaker-story": "\f1c0", + "speaker": "\f1c1", + "spoiler-disable": "\f1c2", + "spoiler": "\f1c3", + "sport": "\f1c4", + "star": "\f1c5", + "stars-lock": "\f1c6", + "stats": "\f1c7", + "stealth-future": "\f1c8", + "stealth-past": "\f1c9", + "stickers": "\f1ca", + "stop-raising-hand": "\f1cb", + "stop": "\f1cc", + "story-caption": "\f1cd", + "story-expired": "\f1ce", + "story-priority": "\f1cf", + "story-reply": "\f1d0", + "strikethrough": "\f1d1", + "tag-add": "\f1d2", + "tag-crossed": "\f1d3", + "tag-filter": "\f1d4", + "tag-name": "\f1d5", + "tag": "\f1d6", + "timer": "\f1d7", + "toncoin": "\f1d8", + "transcribe": "\f1d9", + "truck": "\f1da", + "unarchive": "\f1db", + "underlined": "\f1dc", + "unlock-badge": "\f1dd", + "unlock": "\f1de", + "unmute": "\f1df", + "unpin": "\f1e0", + "unread": "\f1e1", + "up": "\f1e2", + "user-filled": "\f1e3", + "user-online": "\f1e4", + "user": "\f1e5", + "video-outlined": "\f1e6", + "video-stop": "\f1e7", + "video": "\f1e8", + "view-once": "\f1e9", + "voice-chat": "\f1ea", + "volume-1": "\f1eb", + "volume-2": "\f1ec", + "volume-3": "\f1ed", + "web": "\f1ee", + "webapp": "\f1ef", + "word-wrap": "\f1f0", + "zoom-in": "\f1f1", + "zoom-out": "\f1f2", ); .icon-active-sessions::before { @@ -385,6 +387,9 @@ $icons-map: ( .icon-card::before { content: map.get($icons-map, "card"); } +.icon-cash-circle::before { + content: map.get($icons-map, "cash-circle"); +} .icon-channel-filled::before { content: map.get($icons-map, "channel-filled"); } @@ -913,6 +918,9 @@ $icons-map: ( .icon-timer::before { content: map.get($icons-map, "timer"); } +.icon-toncoin::before { + content: map.get($icons-map, "toncoin"); +} .icon-transcribe::before { content: map.get($icons-map, "transcribe"); } diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 2c8012b8a..0dde4c5c1 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 5dc09ac3e..fdd2a37cb 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index 59077e6ec..dec68b118 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -37,6 +37,7 @@ export type FontIconName = | 'camera' | 'car' | 'card' + | 'cash-circle' | 'channel-filled' | 'channel' | 'channelviews' @@ -213,6 +214,7 @@ export type FontIconName = | 'tag-name' | 'tag' | 'timer' + | 'toncoin' | 'transcribe' | 'truck' | 'unarchive' diff --git a/src/types/index.ts b/src/types/index.ts index 663af499c..457c6f018 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -311,6 +311,7 @@ export enum RightColumnContent { AddingMembers, CreateTopic, EditTopic, + MonetizationStatistics, } export type MediaViewerMedia = ApiPhoto | ApiVideo | ApiDocument; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index f59624cf5..8d666e12e 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1512,6 +1512,16 @@ export interface LangPair { 'MenuInstallApp': undefined; 'RemoveEffect': undefined; 'ReplyInPrivateMessage': undefined; + 'MonetizationInfoTONTitle': undefined; + 'ChannelEarnLearnCoinAbout': { + 'link': string | number; + }; + 'MonetizationBalanceZeroInfo': { + 'link': string | number; + }; + 'ChannelEarnAbout': { + 'link': string | number; + }; 'AriaSearchOlderResult': undefined; 'AriaSearchNewerResult': undefined; 'CreditsBoxHistoryEntryGiftOutAbout': {
+ {renderBalanceCell(balances?.availableBalance || 0, usdRate || 0, 'lng_channel_earn_available')} + {renderBalanceCell(balances?.currentBalance || 0, usdRate || 0, 'lng_channel_earn_reward')} + {renderBalanceCell(balances?.overallRevenue || 0, usdRate || 0, 'lng_channel_earn_total')} +