diff --git a/src/api/gramjs/apiBuilders/statistics.ts b/src/api/gramjs/apiBuilders/statistics.ts index cccbae6a9..d96634e59 100644 --- a/src/api/gramjs/apiBuilders/statistics.ts +++ b/src/api/gramjs/apiBuilders/statistics.ts @@ -163,6 +163,8 @@ function buildStatisticsOverview({ current, previous }: GramJs.StatsAbsValueAndP export function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOverviewPercentage { return { + part: data.part, + total: data.total, percentage: ((data.part / data.total) * 100).toFixed(2), }; } diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts index 157bc2f82..f5da16fdd 100644 --- a/src/api/gramjs/apiBuilders/stories.ts +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -3,7 +3,12 @@ import { Api as GramJs, errors } from '../../../lib/gramjs'; import type { ApiApplyBoostInfo, ApiBoostsStatus, - ApiMediaArea, ApiMediaAreaCoordinates, ApiMessage, ApiStealthMode, ApiStoryView, ApiTypeStory, + ApiMediaArea, + ApiMediaAreaCoordinates, + ApiMessage, + ApiStealthMode, + ApiStoryView, + ApiTypeStory, } from '../../types'; import { buildCollectionByCallback } from '../../../util/iteratees'; @@ -215,6 +220,6 @@ export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus): hasMyBoost: Boolean(myBoost), boostUrl, nextLevelBoosts, - ...(premiumAudience && { premiumAudience: buildStatisticsPercentage(premiumAudience) }), + ...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }), }; } diff --git a/src/api/gramjs/methods/stories.ts b/src/api/gramjs/methods/stories.ts index b05ae9963..d6824985b 100644 --- a/src/api/gramjs/methods/stories.ts +++ b/src/api/gramjs/methods/stories.ts @@ -494,3 +494,40 @@ export async function fetchBoostsStatus({ return buildApiBoostsStatus(result); } + +export async function fetchBoostersList({ + chat, + offset = '', + limit, +}: { + chat: ApiChat; + offset?: string; + limit?: number; +}) { + const result = await invokeRequest(new GramJs.stories.GetBoostersList({ + peer: buildInputPeer(chat.id, chat.accessHash), + offset, + limit, + })); + + if (!result) { + return undefined; + } + + addEntitiesToLocalDb(result.users); + + const users = result.users.map(buildApiUser).filter(Boolean); + + const boosterIds = result.boosters.map((booster) => booster.userId.toString()); + const boosters = buildCollectionByCallback(result.boosters, (booster) => ( + [booster.userId.toString(), booster.expires] + )); + + return { + count: result.count, + users, + boosters, + boosterIds, + nextOffset: result.nextOffset, + }; +} diff --git a/src/api/types/statistics.ts b/src/api/types/statistics.ts index 3da5c8cf0..e8546a587 100644 --- a/src/api/types/statistics.ts +++ b/src/api/types/statistics.ts @@ -39,6 +39,13 @@ export interface ApiMessageStatistics { publicForwardsData?: ApiMessagePublicForward[]; } +export interface ApiBoostStatistics { + level: number; + boosts: number; + premiumSubscribers: StatisticsOverviewPercentage; + remainingBoosts: number; +} + export interface ApiMessagePublicForward { messageId: number; views?: number; @@ -76,6 +83,8 @@ export interface StatisticsOverviewItem { } export interface StatisticsOverviewPercentage { + part: number; + total: number; percentage: string; } diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index 6a8ab6d5b..c492b1ca2 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -137,5 +137,5 @@ export type ApiBoostsStatus = { nextLevelBoosts?: number; hasMyBoost?: boolean; boostUrl: string; - premiumAudience?: StatisticsOverviewPercentage; + premiumSubscribers?: StatisticsOverviewPercentage; }; diff --git a/src/components/common/InviteLink.module.scss b/src/components/common/InviteLink.module.scss index 99f22f0c1..595f76b5b 100644 --- a/src/components/common/InviteLink.module.scss +++ b/src/components/common/InviteLink.module.scss @@ -17,12 +17,7 @@ z-index: 1; } -.buttons { - display: flex; - gap: 1rem; -} - -.button { - width: auto; - flex: 1 0 auto; +.title { + font-weight: 500; + color: var(--color-text-secondary); } diff --git a/src/components/common/InviteLink.tsx b/src/components/common/InviteLink.tsx index 5dd72cea6..9dda31f87 100644 --- a/src/components/common/InviteLink.tsx +++ b/src/components/common/InviteLink.tsx @@ -18,15 +18,17 @@ import styles from './InviteLink.module.scss'; type OwnProps = { title?: string; inviteLink: string; - onRevoke?: VoidFunction; isDisabled?: boolean; + className?: string; + onRevoke?: VoidFunction; }; const InviteLink: FC = ({ title, inviteLink, - onRevoke, isDisabled, + className, + onRevoke, }) => { const lang = useLang(); const { showNotification, openChatWithDraft } = getActions(); @@ -66,8 +68,8 @@ const InviteLink: FC = ({ }, [isMobile, lang]); return ( -
-

+

+

{lang(title || 'InviteLink.InviteLink')}

@@ -88,24 +90,13 @@ const InviteLink: FC = ({ )}
-
- - -
+
); }; diff --git a/src/components/common/ManageUsernames.module.scss b/src/components/common/ManageUsernames.module.scss index 6c113e81f..ac2aa229d 100644 --- a/src/components/common/ManageUsernames.module.scss +++ b/src/components/common/ManageUsernames.module.scss @@ -1,8 +1,10 @@ +@import "../../styles/mixins"; + .container { - background-color: var(--color-background); padding: 1.5rem 1.5rem 0; - box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent); margin-bottom: 0.625rem; + + @include side-panel-section; } .header { diff --git a/src/components/common/PremiumProgress.module.scss b/src/components/common/PremiumProgress.module.scss index b7d73f79c..108d24479 100644 --- a/src/components/common/PremiumProgress.module.scss +++ b/src/components/common/PremiumProgress.module.scss @@ -36,16 +36,12 @@ } } -.floating-badge { - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - position: relative; - padding: 0.25rem 0.75rem; - border-radius: 1rem; - background-color: #7E85FF; +.floating-badge-wrapper { animation: rotate-in 0.5s ease-in-out; + border-radius: 0.5rem; + height: 2.6875rem; + position: relative; + overflow: hidden; } @keyframes rotate-in { @@ -63,9 +59,24 @@ } } +.floating-badge { + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + padding: 0.25rem 0.75rem; + border-radius: 1rem; + background-color: #7E85FF; + position: relative; + z-index: 1; +} + .floating-badge-triangle { + display: inline-block; position: absolute; - bottom: -15px; + bottom: -5px; + left: calc(var(--tail-position, 0.5) * 100%); + transform: translateX(-50%); } .floating-badge-icon { diff --git a/src/components/common/PremiumProgress.tsx b/src/components/common/PremiumProgress.tsx index 47e0f985b..9b091e2eb 100644 --- a/src/components/common/PremiumProgress.tsx +++ b/src/components/common/PremiumProgress.tsx @@ -21,6 +21,8 @@ type OwnProps = { className?: string; }; +const PROGRESS_LOCK = 0.1; + const LimitPreview: FC = ({ leftText, rightText, @@ -34,6 +36,8 @@ const LimitPreview: FC = ({ const hasFloatingBadge = Boolean(floatingBadgeIcon || floatingBadgeText); const isProgressFull = Boolean(progress) && progress > 0.99; + const tailPosition = progress && (progress < PROGRESS_LOCK ? 0 : progress > 1 - PROGRESS_LOCK ? 1 : 0.5); + return (
= ({ hasFloatingBadge && styles.withBadge, className, )} - style={buildStyle(progress !== undefined && `--progress: ${progress}`)} + style={buildStyle( + progress !== undefined && `--progress: ${progress}`, + tailPosition !== undefined && `--tail-position: ${tailPosition}`, + )} > {hasFloatingBadge && (
-
- {floatingBadgeIcon && } - {floatingBadgeText && ( -
{floatingBadgeText}
- )} +
+
+ {floatingBadgeIcon && } + {floatingBadgeText && ( +
+ {floatingBadgeText} +
+ )} +
- - + +
diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 6261bdd21..a57151c2e 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -43,6 +43,7 @@ type OwnProps = { noStatusOrTyping?: boolean; noRtl?: boolean; adminMember?: ApiChatMember; + className?: string; onEmojiStatusClick?: NoneToVoidFunction; }; @@ -75,8 +76,9 @@ const PrivateChatInfo: FC = ({ areMessagesLoaded, adminMember, ripple, - onEmojiStatusClick, + className, storyViewerOrigin, + onEmojiStatusClick, }) => { const { loadFullUser, @@ -180,7 +182,7 @@ const PrivateChatInfo: FC = ({ } return ( -
+
.ChatExtra { padding: 0 0.5rem 0.3125rem; margin: 0 -0.5rem 0.625rem; - box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent); - border-bottom: 0.625rem solid var(--color-background-secondary); + @include side-panel-section; .ListItem.narrow { margin-bottom: 0.25rem; @@ -130,15 +124,8 @@ .settings-item-simple, .settings-item { - background-color: var(--color-background); padding: 1.5rem 1.5rem 1rem; - box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent); - border-bottom: 0.625rem solid var(--color-background-secondary); - - &:last-child { - border-bottom: none; - box-shadow: none; - } + @include side-panel-section; } .settings-item { diff --git a/src/components/left/settings/SettingsGeneralBackground.scss b/src/components/left/settings/SettingsGeneralBackground.scss index 41e5ee9de..47c345aad 100644 --- a/src/components/left/settings/SettingsGeneralBackground.scss +++ b/src/components/left/settings/SettingsGeneralBackground.scss @@ -1,11 +1,12 @@ +@import "../../../styles/mixins"; + .SettingsGeneralBackground { .settings-wallpapers { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; grid-gap: 0.0625rem; - background-color: var(--color-background); - box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent); + @include side-panel-section; } .Loading { diff --git a/src/components/left/settings/SettingsGeneralBackgroundColor.scss b/src/components/left/settings/SettingsGeneralBackgroundColor.scss index 1364e4404..3d738bb8b 100644 --- a/src/components/left/settings/SettingsGeneralBackgroundColor.scss +++ b/src/components/left/settings/SettingsGeneralBackgroundColor.scss @@ -1,3 +1,5 @@ +@import "../../../styles/mixins"; + .SettingsGeneralBackgroundColor { &:not(.is-dragging) .handle { transition: transform 300ms ease; @@ -69,8 +71,7 @@ grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; grid-gap: 0.0625rem; - background-color: var(--color-background); - box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent); + @include side-panel-section; } .predefined-color { diff --git a/src/components/left/settings/folders/SettingsShareChatlist.tsx b/src/components/left/settings/folders/SettingsShareChatlist.tsx index 7703add5b..574202d60 100644 --- a/src/components/left/settings/folders/SettingsShareChatlist.tsx +++ b/src/components/left/settings/folders/SettingsShareChatlist.tsx @@ -160,6 +160,7 @@ const SettingsShareChatlist: FC = ({
= ({ canCall, canMute, canViewStatistics, + canViewBoosts, canLeave, canEnterVoiceChat, canCreateVoiceChat, @@ -425,6 +428,7 @@ const HeaderActions: FC = ({ canCall={canCall} canMute={canMute} canViewStatistics={canViewStatistics} + canViewBoosts={canViewBoosts} canLeave={canLeave} canEnterVoiceChat={canEnterVoiceChat} canCreateVoiceChat={canCreateVoiceChat} @@ -486,6 +490,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 canViewBoosts = isMainThread && isChannel && (canViewStatistics || getHasAdminRight(chat, 'postStories')); const pendingJoinRequests = isMainThread ? chatFullInfo?.requestsPending : undefined; const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend); const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest); @@ -505,6 +510,7 @@ export default memo(withGlobal( canCall, canMute, canViewStatistics, + canViewBoosts, canLeave, canEnterVoiceChat, canCreateVoiceChat, diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 6e592fd9f..60c61b656 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -81,6 +81,7 @@ export type OwnProps = { canCall?: boolean; canMute?: boolean; canViewStatistics?: boolean; + canViewBoosts?: boolean; withForumActions?: boolean; canLeave?: boolean; canEnterVoiceChat?: boolean; @@ -137,6 +138,7 @@ const HeaderMenuContainer: FC = ({ canCall, canMute, canViewStatistics, + canViewBoosts, pendingJoinRequests, canLeave, canEnterVoiceChat, @@ -174,6 +176,7 @@ const HeaderMenuContainer: FC = ({ openAddContactDialog, requestMasterAndRequestCall, toggleStatistics, + openBoostStatistics, openGiftPremiumModal, openChatWithInfo, openCreateTopicPanel, @@ -332,6 +335,12 @@ const HeaderMenuContainer: FC = ({ closeMenu(); }); + const handleBoostClick = useLastCallback(() => { + openBoostStatistics({ chatId }); + setShouldCloseFast(!isRightColumnShown); + closeMenu(); + }); + const handleEnableTranslations = useLastCallback(() => { togglePeerTranslations({ chatId, isEnabled: true }); closeMenu(); @@ -546,6 +555,14 @@ const HeaderMenuContainer: FC = ({ {lang('ReportSelectMessages')} )} + {canViewBoosts && ( + + {lang('Boosts')} + + )} {canViewStatistics && ( = ({ - onClose, isActive, chat, message, + onClose, }) => { const lang = useLang(); diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 8ac859bb9..42f366b2d 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -1,3 +1,5 @@ +@import '../../styles/mixins'; + .Profile { height: 100%; display: flex; @@ -16,8 +18,7 @@ > .profile-info > .ChatExtra { padding: 0.875rem 0.5rem 0.5rem; - box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent); - border-bottom: 0.625rem solid var(--color-background-secondary); + @include side-panel-section; .narrow { margin-bottom: 0; diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index 9699affea..c0e0e85db 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -30,6 +30,7 @@ import PollResults from './PollResults.async'; import Profile from './Profile'; import RightHeader from './RightHeader'; import RightSearch from './RightSearch.async'; +import BoostStatistics from './statistics/BoostStatistics'; import MessageStatistics from './statistics/MessageStatistics.async'; import Statistics from './statistics/Statistics.async'; import StickerSearch from './StickerSearch.async'; @@ -90,6 +91,7 @@ const RightColumn: FC = ({ resetNextProfileTab, closeCreateTopicPanel, closeEditTopicPanel, + closeBoostStatistics, } = getActions(); const { width: windowWidth } = useWindowSize(); @@ -105,6 +107,7 @@ const RightColumn: FC = ({ const isManagement = contentKey === RightColumnContent.Management; const isStatistics = contentKey === RightColumnContent.Statistics; const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics; + const isBoostStatistics = contentKey === RightColumnContent.BoostStatistics; const isStickerSearch = contentKey === RightColumnContent.StickerSearch; const isGifSearch = contentKey === RightColumnContent.GifSearch; const isPollResults = contentKey === RightColumnContent.PollResults; @@ -176,6 +179,9 @@ const RightColumn: FC = ({ case RightColumnContent.Statistics: toggleStatistics(); break; + case RightColumnContent.BoostStatistics: + closeBoostStatistics(); + break; case RightColumnContent.Search: { blurSearchInput(); closeLocalTextSearch(); @@ -312,6 +318,8 @@ const RightColumn: FC = ({ case RightColumnContent.Statistics: return ; + case RightColumnContent.BoostStatistics: + return ; case RightColumnContent.MessageStatistics: return ; case RightColumnContent.StickerSearch: @@ -346,6 +354,7 @@ const RightColumn: FC = ({ isSearch={isSearch} isManagement={isManagement} isStatistics={isStatistics} + isBoostStatistics={isBoostStatistics} isMessageStatistics={isMessageStatistics} isStickerSearch={isStickerSearch} isGifSearch={isGifSearch} diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 0b50e8ffb..a9d4d2043 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -46,6 +46,7 @@ type OwnProps = { isSearch?: boolean; isManagement?: boolean; isStatistics?: boolean; + isBoostStatistics?: boolean; isMessageStatistics?: boolean; isStickerSearch?: boolean; isGifSearch?: boolean; @@ -88,6 +89,7 @@ enum HeaderContent { Search, Statistics, MessageStatistics, + BoostStatistics, Management, ManageInitial, ManageChannelSubscribers, @@ -126,6 +128,7 @@ const RightHeader: FC = ({ isManagement, isStatistics, isMessageStatistics, + isBoostStatistics, isStickerSearch, isGifSearch, isPollResults, @@ -139,8 +142,6 @@ const RightHeader: FC = ({ isSelf, canManage, isChannel, - onClose, - onScreenSelect, messageSearchQuery, stickerSearchQuery, gifSearchQuery, @@ -151,6 +152,8 @@ const RightHeader: FC = ({ isBot, isInsideTopic, canEditTopic, + onClose, + onScreenSelect, }) => { const { setLocalTextSearchQuery, @@ -288,6 +291,8 @@ const RightHeader: FC = ({ HeaderContent.Statistics ) : isMessageStatistics ? ( HeaderContent.MessageStatistics + ) : isBoostStatistics ? ( + HeaderContent.BoostStatistics ) : isCreatingTopic ? ( HeaderContent.CreateTopic ) : isEditingTopic ? ( @@ -437,6 +442,8 @@ const RightHeader: FC = ({ return

{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}

; case HeaderContent.MessageStatistics: return

{lang('Stats.MessageTitle')}

; + case HeaderContent.BoostStatistics: + return

{lang('Boosts')}

; case HeaderContent.SharedMedia: return

{lang('SharedMedia')}

; case HeaderContent.ManageChannelSubscribers: diff --git a/src/components/right/management/ManageInvites.tsx b/src/components/right/management/ManageInvites.tsx index 0eee755bb..eb0ee10fa 100644 --- a/src/components/right/management/ManageInvites.tsx +++ b/src/components/right/management/ManageInvites.tsx @@ -284,6 +284,7 @@ const ManageInvites: FC = ({
{primaryInviteLink && ( { + const { openChat, loadMoreBoosters, closeBoostStatistics } = getActions(); + const lang = useLang(); + + const isLoaded = boostStatistics?.boostStatus; + const status = isLoaded ? boostStatistics.boostStatus : undefined; + + const { + currentLevel, + hasNextLevel, + boosts, + levelProgress, + remainingBoosts, + } = useMemo(() => { + if (!status) { + return { + currentLevel: 0, + hasNextLevel: false, + boosts: 0, + levelProgress: 0, + remainingBoosts: 0, + }; + } + return getBoostProgressInfo(status); + }, [status]); + + const statsOverview = useMemo(() => { + if (!status) return undefined; + + return { + level: currentLevel, + boosts, + premiumSubscribers: status.premiumSubscribers!, + remainingBoosts, + } satisfies ApiBoostStatistics; + }, [status, boosts, currentLevel, remainingBoosts]); + + const boostersToLoadCount = useMemo(() => { + if (!boostStatistics?.count) return undefined; + const loadedCount = boostStatistics.boosterIds?.length || 0; + const totalCount = boostStatistics.count; + return totalCount - loadedCount; + }, [boostStatistics]); + + const handleBoosterClick = useLastCallback((userId: string) => { + openChat({ id: userId }); + closeBoostStatistics(); + }); + + const handleLoadMore = useLastCallback(() => { + loadMoreBoosters(); + }); + + return ( +
+ {!isLoaded && } + {isLoaded && statsOverview && ( + <> +
+ + +
+
+

+ {lang('Boosters')} +

+ {!boostStatistics.boosterIds?.length && ( +
{lang('NoBoostersHint')}
+ )} + {boostStatistics.boosterIds?.map((userId) => ( + handleBoosterClick(userId)} + > + + + ))} + {Boolean(boostersToLoadCount) && ( + + {boostStatistics?.isLoadingBoosters ? ( + + ) : ( + + )} + {lang('ShowVotes', boostersToLoadCount)} + + )} +
+ + + )} +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const tabState = selectTabState(global); + const boostStatistics = tabState.boostStatistics; + + return { + boostStatistics, + }; + }, +)(BoostStatistics)); diff --git a/src/components/right/statistics/MessageStatistics.tsx b/src/components/right/statistics/MessageStatistics.tsx index 20dd86191..cd5339903 100644 --- a/src/components/right/statistics/MessageStatistics.tsx +++ b/src/components/right/statistics/MessageStatistics.tsx @@ -150,7 +150,7 @@ const Statistics: FC = ({ return (
- + {!loadedCharts.current.length && } diff --git a/src/components/right/statistics/Statistics.tsx b/src/components/right/statistics/Statistics.tsx index cd8e04424..13f585d1f 100644 --- a/src/components/right/statistics/Statistics.tsx +++ b/src/components/right/statistics/Statistics.tsx @@ -178,7 +178,11 @@ const Statistics: FC = ({ return (
- + {!loadedCharts.current.length && } diff --git a/src/components/right/statistics/StatisticsOverview.scss b/src/components/right/statistics/StatisticsOverview.scss index 4ae7efbab..7eb85b769 100644 --- a/src/components/right/statistics/StatisticsOverview.scss +++ b/src/components/right/statistics/StatisticsOverview.scss @@ -42,7 +42,13 @@ &-value { font-weight: 500; - font-size: 1rem; + font-size: 1.25rem; + } + + &-secondary-value { + font-size: 0.875rem; + color: var(--color-text-secondary); + margin-inline-start: 0.25rem; } } diff --git a/src/components/right/statistics/StatisticsOverview.tsx b/src/components/right/statistics/StatisticsOverview.tsx index 4ebddc2f2..ea4ba01d0 100644 --- a/src/components/right/statistics/StatisticsOverview.tsx +++ b/src/components/right/statistics/StatisticsOverview.tsx @@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo } from '../../../lib/teact/teact'; import type { + ApiBoostStatistics, ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsOverviewItem, } from '../../../api/types'; @@ -17,6 +18,7 @@ type OverviewCell = { name: string; title: string; isPercentage?: boolean; + withAbsoluteValue?: boolean; isPlain?: boolean; isApproximate?: boolean; }; @@ -55,13 +57,38 @@ const MESSAGE_OVERVIEW: OverviewCell[][] = [ ], ]; +const BOOST_OVERVIEW: OverviewCell[][] = [ + [ + { name: 'level', title: 'Stats.Boosts.Level', isPlain: true }, + { + name: 'premiumSubscribers', + title: 'Stats.Boosts.PremiumSubscribers', + isPercentage: true, + isApproximate: true, + withAbsoluteValue: true, + }, + ], + [ + { name: 'boosts', title: 'Stats.Boosts.ExistingBoosts', isPlain: true }, + { name: 'remainingBoosts', title: 'Stats.Boosts.BoostsToLevelUp', isPlain: true }, + ], +]; + +type StatisticsType = 'channel' | 'group' | 'message' | 'boost'; + export type OwnProps = { - isGroup?: boolean; - isMessage?: boolean; - statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics; + type: StatisticsType; + title?: string; + className?: string; + statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics | ApiBoostStatistics; }; -const StatisticsOverview: FC = ({ isGroup, isMessage, statistics }) => { +const StatisticsOverview: FC = ({ + title, + type, + statistics, + className, +}) => { const lang = useLang(); const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => { @@ -86,10 +113,17 @@ const StatisticsOverview: FC = ({ isGroup, isMessage, statistics }) => const { period } = (statistics as ApiGroupStatistics); + const schema = type === 'boost' ? BOOST_OVERVIEW : type === 'message' ? MESSAGE_OVERVIEW : type === 'group' + ? GROUP_OVERVIEW : CHANNEL_OVERVIEW; + return ( -
+
-
{lang('StatisticOverview')}
+ {title && ( +
+ {title} +
+ )} {period && (
@@ -99,7 +133,7 @@ const StatisticsOverview: FC = ({ isGroup, isMessage, statistics }) =>
- {(isMessage ? MESSAGE_OVERVIEW : isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => ( + {schema.map((row) => ( {row.map((cell: OverviewCell) => { const field = (statistics as any)[cell.name]; @@ -108,7 +142,7 @@ const StatisticsOverview: FC = ({ isGroup, isMessage, statistics }) => return ( @@ -118,7 +152,14 @@ const StatisticsOverview: FC = ({ isGroup, isMessage, statistics }) => if (cell.isPercentage) { return ( ); diff --git a/src/components/ui/ListItem.scss b/src/components/ui/ListItem.scss index dfdafdb04..ba49e02aa 100644 --- a/src/components/ui/ListItem.scss +++ b/src/components/ui/ListItem.scss @@ -82,7 +82,8 @@ > .icon { font-size: 1.5rem; - margin-right: 2rem; + margin-inline-start: 0.125rem; + margin-inline-end: 1.25rem; color: var(--color-text-secondary); } @@ -489,13 +490,6 @@ } } } - - &[dir="rtl"] { - .ListItem-button > .icon { - margin-left: 2rem; - margin-right: 0; - } - } } .list-item-ellipsis { diff --git a/src/global/actions/api/stories.ts b/src/global/actions/api/stories.ts index d1d514ed8..c75d5bff4 100644 --- a/src/global/actions/api/stories.ts +++ b/src/global/actions/api/stories.ts @@ -2,7 +2,7 @@ import type { ActionReturnType } from '../../types'; import { DEBUG, PREVIEW_AVATAR_COUNT } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; -import { buildCollectionByKey } from '../../../util/iteratees'; +import { buildCollectionByKey, unique } from '../../../util/iteratees'; import { translate } from '../../../util/langProvider'; import { getServerTime } from '../../../util/serverTime'; import { callApi } from '../../../api/gramjs'; @@ -557,6 +557,91 @@ addActionHandler('openBoostModal', async (global, actions, payload): Promise => { + const { chatId, tabId = getCurrentTabId() } = payload; + + const chat = selectChat(global, chatId); + if (!chat) return; + + global = updateTabState(global, { + boostStatistics: { + chatId, + }, + }, tabId); + setGlobal(global); + + const [boostersListResult, boostStatusResult] = await Promise.all([ + callApi('fetchBoostersList', { chat }), + callApi('fetchBoostsStatus', { chat }), + ]); + + global = getGlobal(); + if (!boostersListResult || !boostStatusResult) { + global = updateTabState(global, { + boostStatistics: undefined, + }, tabId); + setGlobal(global); + return; + } + + global = addUsers(global, buildCollectionByKey(boostersListResult.users, 'id')); + global = updateTabState(global, { + boostStatistics: { + chatId, + boostStatus: boostStatusResult, + boosters: boostersListResult.boosters, + boosterIds: boostersListResult.boosterIds, + count: boostersListResult.count, + nextOffset: boostersListResult.nextOffset, + }, + }, tabId); + setGlobal(global); +}); + +addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise => { + const { tabId = getCurrentTabId() } = payload || {}; + let tabState = selectTabState(global, tabId); + if (!tabState.boostStatistics) return; + + const chat = selectChat(global, tabState.boostStatistics.chatId); + if (!chat) return; + + global = updateTabState(global, { + boostStatistics: { + ...tabState.boostStatistics, + isLoadingBoosters: true, + }, + }, tabId); + setGlobal(global); + + const result = await callApi('fetchBoostersList', { + chat, + offset: tabState.boostStatistics.nextOffset, + }); + if (!result) return; + + global = getGlobal(); + global = addUsers(global, buildCollectionByKey(result.users, 'id')); + + tabState = selectTabState(global, tabId); + if (!tabState.boostStatistics) return; + + global = updateTabState(global, { + boostStatistics: { + ...tabState.boostStatistics, + boosters: { + ...tabState.boostStatistics.boosters, + ...result.boosters, + }, + boosterIds: unique([...tabState.boostStatistics.boosterIds || [], ...result.boosterIds]), + count: result.count, + nextOffset: result.nextOffset, + isLoadingBoosters: false, + }, + }, tabId); + setGlobal(global); +}); + addActionHandler('applyBoost', async (global, actions, payload): Promise => { const { chatId, tabId = getCurrentTabId() } = payload; diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index c7db0a7c8..ab38c5d4f 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -55,6 +55,7 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => { global = updateTabState(global, { isStatisticsShown: false, + boostStatistics: undefined, contentToBeScheduled: undefined, ...(id !== selectTabState(global, tabId).forwardMessages.toChatId && { forwardMessages: {}, diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index 9024b7cd9..62ff654b2 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -419,3 +419,11 @@ addActionHandler('closeBoostModal', (global, actions, payload): ActionReturnType boostModal: undefined, }, tabId); }); + +addActionHandler('closeBoostStatistics', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + boostStatistics: undefined, + }, tabId); +}); diff --git a/src/global/selectors/ui.ts b/src/global/selectors/ui.ts index e822f4297..595d5bc12 100644 --- a/src/global/selectors/ui.ts +++ b/src/global/selectors/ui.ts @@ -39,6 +39,8 @@ export function selectRightColumnContentKey( RightColumnContent.MessageStatistics ) : selectIsStatisticsShown(global, tabId) ? ( RightColumnContent.Statistics + ) : tabState.boostStatistics ? ( + RightColumnContent.BoostStatistics ) : tabState.stickerSearch.query !== undefined ? ( RightColumnContent.StickerSearch ) : tabState.gifSearch.query !== undefined ? ( diff --git a/src/global/types.ts b/src/global/types.ts index 32ebaf39e..c38cb818d 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -625,6 +625,16 @@ export type TabState = { boostStatus?: ApiBoostsStatus; applyInfo?: ApiApplyBoostInfo; }; + + boostStatistics?: { + chatId: string; + boosters?: Record; + boosterIds?: string[]; + boostStatus?: ApiBoostsStatus; + isLoadingBoosters?: boolean; + nextOffset?: string; + count?: number; + }; }; export type GlobalState = { @@ -2084,6 +2094,11 @@ export interface ActionPayloads { chatId: string; } & WithTabId; closeBoostModal: WithTabId | undefined; + openBoostStatistics: { + chatId: string; + } & WithTabId; + closeBoostStatistics: WithTabId | undefined; + loadMoreBoosters: WithTabId | undefined; applyBoost: { chatId: string; } & WithTabId; diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 3b087cc2a..0dacb53da 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -348,7 +348,7 @@ namespace Api { export type TypeAccessPointRule = AccessPointRule; export type TypeTlsClientHello = TlsClientHello; export type TypeTlsBlock = TlsBlockString | TlsBlockRandom | TlsBlockZero | TlsBlockDomain | TlsBlockGrease | TlsBlockScope; - + export namespace storage { export type TypeFileType = storage.FileUnknown | storage.FilePartial | storage.FileJpeg | storage.FileGif | storage.FilePng | storage.FilePdf | storage.FileMp3 | storage.FileMov | storage.FileMp4 | storage.FileWebp; @@ -8916,7 +8916,7 @@ namespace Api { }> { entries: Api.TypeTlsBlock[]; }; - + export namespace storage { export class FileUnknown extends VirtualClass {}; @@ -10983,7 +10983,7 @@ namespace Api { }>, Api.TypeDestroySessionRes> { sessionId: long; }; - + export namespace auth { export class SendCode extends Request
- {cell.isApproximate ? `≈${formatInteger(field)}` : formatInteger(field)} + {`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}

{lang(cell.title)}

- {field.percentage}% + {cell.withAbsoluteValue && ( + + {`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`} + + )} + + {field.percentage}% +

{lang(cell.title)}