From fb79e088fe6f8c9a79b78792b4a144e9f0c8e2c3 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 21 Aug 2025 12:05:42 +0200 Subject: [PATCH] Statistics: Fix statistics overview (#5963) --- src/api/gramjs/apiBuilders/statistics.ts | 51 +++++++++------- src/api/gramjs/methods/messages.ts | 24 ++++++++ src/api/gramjs/methods/statistics.ts | 14 +++-- src/api/types/statistics.ts | 59 ++++++++++++------- src/components/middle/message/Message.tsx | 6 +- .../right/statistics/MessageStatistics.tsx | 56 +++++++++++------- .../statistics/MonetizationStatistics.tsx | 29 +++++---- .../right/statistics/Statistics.module.scss | 2 +- .../right/statistics/Statistics.tsx | 45 +++++++++----- .../right/statistics/StatisticsOverview.tsx | 15 +++-- .../right/statistics/StoryStatistics.tsx | 52 ++++++++++------ .../right/statistics/helpers/isGraph.ts | 6 ++ src/config.ts | 1 - src/global/actions/api/messages.ts | 18 ++++++ src/global/actions/api/statistics.ts | 28 ++++++--- src/global/actions/ui/misc.ts | 4 ++ src/global/types/actions.ts | 6 +- 17 files changed, 288 insertions(+), 128 deletions(-) create mode 100644 src/components/right/statistics/helpers/isGraph.ts diff --git a/src/api/gramjs/apiBuilders/statistics.ts b/src/api/gramjs/apiBuilders/statistics.ts index 281340746..9a7f99a70 100644 --- a/src/api/gramjs/apiBuilders/statistics.ts +++ b/src/api/gramjs/apiBuilders/statistics.ts @@ -8,12 +8,12 @@ import type { ApiPostStatistics, ApiStoryPublicForward, ChannelMonetizationBalances, - StatisticsGraph, StatisticsMessageInteractionCounter, StatisticsOverviewItem, StatisticsOverviewPercentage, StatisticsOverviewPeriod, StatisticsStoryInteractionCounter, + TypeStatisticsGraph, } from '../../types'; import { buildApiUsernames, buildAvatarPhotoId } from './common'; @@ -23,20 +23,19 @@ const DECIMALS = 10 ** 9; export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics { return { + type: 'channel', // Graphs growthGraph: buildGraph(stats.growthGraph), followersGraph: buildGraph(stats.followersGraph), muteGraph: buildGraph(stats.muteGraph), topHoursGraph: buildGraph(stats.topHoursGraph), - - // Async graphs - languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token, - viewsBySourceGraph: (stats.viewsBySourceGraph as GramJs.StatsGraphAsync).token, - newFollowersBySourceGraph: (stats.newFollowersBySourceGraph as GramJs.StatsGraphAsync).token, - interactionsGraph: (stats.interactionsGraph as GramJs.StatsGraphAsync).token, - reactionsByEmotionGraph: (stats.reactionsByEmotionGraph as GramJs.StatsGraphAsync).token, - storyInteractionsGraph: (stats.storyInteractionsGraph as GramJs.StatsGraphAsync).token, - storyReactionsByEmotionGraph: (stats.storyReactionsByEmotionGraph as GramJs.StatsGraphAsync).token, + languagesGraph: buildGraph(stats.languagesGraph), + viewsBySourceGraph: buildGraph(stats.viewsBySourceGraph), + newFollowersBySourceGraph: buildGraph(stats.newFollowersBySourceGraph), + interactionsGraph: buildGraph(stats.interactionsGraph), + reactionsByEmotionGraph: buildGraph(stats.reactionsByEmotionGraph), + storyInteractionsGraph: buildGraph(stats.storyInteractionsGraph), + storyReactionsByEmotionGraph: buildGraph(stats.storyReactionsByEmotionGraph), // Statistics overview followers: buildStatisticsOverview(stats.followers), @@ -72,6 +71,7 @@ export function buildApiPostInteractionCounter( ): StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter | undefined { if (interaction instanceof GramJs.PostInteractionCountersMessage) { return { + type: 'message', msgId: interaction.msgId, forwardsCount: interaction.forwards, viewsCount: interaction.views, @@ -81,6 +81,7 @@ export function buildApiPostInteractionCounter( if (interaction instanceof GramJs.PostInteractionCountersStory) { return { + type: 'story', storyId: interaction.storyId, reactionsCount: interaction.reactions, viewsCount: interaction.views, @@ -93,15 +94,14 @@ export function buildApiPostInteractionCounter( export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGroupStatistics { return { + type: 'group', // Graphs growthGraph: buildGraph(stats.growthGraph), membersGraph: buildGraph(stats.membersGraph), topHoursGraph: buildGraph(stats.topHoursGraph), - - // Async graphs - languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token, - messagesGraph: (stats.messagesGraph as GramJs.StatsGraphAsync).token, - actionsGraph: (stats.actionsGraph as GramJs.StatsGraphAsync).token, + languagesGraph: buildGraph(stats.languagesGraph), + messagesGraph: buildGraph(stats.messagesGraph), + actionsGraph: buildGraph(stats.actionsGraph), // Statistics overview period: getOverviewPeriod(stats.period), @@ -158,18 +158,29 @@ export function buildStoryPublicForwards( export function buildGraph( result: GramJs.TypeStatsGraph, isPercentage?: boolean, isCurrency?: boolean, currencyRate?: number, -): StatisticsGraph | undefined { - if ((result as GramJs.StatsGraphError).error) { - return undefined; +): TypeStatisticsGraph { + if (result instanceof GramJs.StatsGraphError) { + return { + graphType: 'error', + error: result.error, + }; } - const data = JSON.parse((result as GramJs.StatsGraph).json.data); + if (result instanceof GramJs.StatsGraphAsync) { + return { + graphType: 'async', + token: result.token, + }; + } + + const data = JSON.parse(result.json.data); const [x, ...y] = data.columns; const hasSecondYAxis = data.y_scaled; return { + graphType: 'graph', type: isPercentage ? 'area' : data.types.y0, - zoomToken: (result as GramJs.StatsGraph).zoomToken, + zoomToken: result.zoomToken, labelFormatter: data.xTickFormatter, tooltipFormatter: data.xTooltipFormatter, labels: x.slice(1), diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 8caebfd0a..8974d3d1d 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -270,6 +270,30 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message return { message }; } +export async function fetchMessagesById({ chat, messageIds }: { chat: ApiChat; messageIds: number[] }) { + const isChannel = getEntityTypeById(chat.id) === 'channel'; + + const result = await invokeRequest( + isChannel + ? new GramJs.channels.GetMessages({ + channel: buildInputChannel(chat.id, chat.accessHash), + id: messageIds.map((id) => new GramJs.InputMessageID({ id })), + }) + : new GramJs.messages.GetMessages({ + id: messageIds.map((id) => new GramJs.InputMessageID({ id })), + }), + { + shouldThrow: true, + }, + ); + + if (!result || result instanceof GramJs.messages.MessagesNotModified) { + return undefined; + } + + return result.messages.map(buildApiMessage).filter(Boolean); +} + let mediaQueue = Promise.resolve(); export function sendMessageLocal( diff --git a/src/api/gramjs/methods/statistics.ts b/src/api/gramjs/methods/statistics.ts index eaa769f90..5ea859459 100644 --- a/src/api/gramjs/methods/statistics.ts +++ b/src/api/gramjs/methods/statistics.ts @@ -5,7 +5,6 @@ import type { ApiChat, ApiMessagePublicForward, ApiPeer, ApiPostStatistics, ApiStoryPublicForward, StatisticsGraph, } from '../../types'; -import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config'; import { buildChannelMonetizationStatistics, buildChannelStatistics, @@ -104,11 +103,13 @@ export async function fetchMessagePublicForwards({ messageId, dcId, offset = DEFAULT_PRIMITIVES.STRING, + limit = DEFAULT_PRIMITIVES.INT, }: { chat: ApiChat; messageId: number; dcId?: number; offset?: string; + limit?: number; }): Promise<{ forwards?: ApiMessagePublicForward[]; count?: number; @@ -118,7 +119,7 @@ export async function fetchMessagePublicForwards({ channel: buildInputChannel(chat.id, chat.accessHash), msgId: messageId, offset, - limit: STATISTICS_PUBLIC_FORWARDS_LIMIT, + limit, }), { dcId, }); @@ -156,7 +157,10 @@ export async function fetchStatisticsAsyncGraph({ return undefined; } - return buildGraph(result as GramJs.StatsGraph, isPercentage); + const graph = buildGraph(result, isPercentage); + + if (graph.graphType !== 'graph') return undefined; + return graph; } export async function fetchStoryStatistics({ @@ -187,11 +191,13 @@ export async function fetchStoryPublicForwards({ storyId, dcId, offset = DEFAULT_PRIMITIVES.STRING, + limit = DEFAULT_PRIMITIVES.INT, }: { chat: ApiChat; storyId: number; dcId?: number; offset?: string; + limit?: number; }): Promise<{ publicForwards: (ApiMessagePublicForward | ApiStoryPublicForward)[] | undefined; count?: number; @@ -201,7 +207,7 @@ export async function fetchStoryPublicForwards({ peer: buildInputPeer(chat.id, chat.accessHash), id: storyId, offset, - limit: STATISTICS_PUBLIC_FORWARDS_LIMIT, + limit, }), { dcId, }); diff --git a/src/api/types/statistics.ts b/src/api/types/statistics.ts index 5f9bfabf2..80ae47566 100644 --- a/src/api/types/statistics.ts +++ b/src/api/types/statistics.ts @@ -2,17 +2,18 @@ import type { ApiChat } from './chats'; import type { ApiTypePrepaidGiveaway } from './payments'; export interface ApiChannelStatistics { - growthGraph?: StatisticsGraph | string; - followersGraph?: StatisticsGraph | string; - muteGraph?: StatisticsGraph | string; - topHoursGraph?: StatisticsGraph | string; - reactionsByEmotionGraph?: StatisticsGraph | string; - storyInteractionsGraph?: StatisticsGraph | string; - storyReactionsByEmotionGraph?: StatisticsGraph | string; - interactionsGraph: StatisticsGraph | string; - viewsBySourceGraph: StatisticsGraph | string; - newFollowersBySourceGraph: StatisticsGraph | string; - languagesGraph: StatisticsGraph | string; + type: 'channel'; + growthGraph?: TypeStatisticsGraph; + followersGraph?: TypeStatisticsGraph; + muteGraph?: TypeStatisticsGraph; + topHoursGraph?: TypeStatisticsGraph; + reactionsByEmotionGraph?: TypeStatisticsGraph; + storyInteractionsGraph?: TypeStatisticsGraph; + storyReactionsByEmotionGraph?: TypeStatisticsGraph; + interactionsGraph: TypeStatisticsGraph; + viewsBySourceGraph: TypeStatisticsGraph; + newFollowersBySourceGraph: TypeStatisticsGraph; + languagesGraph: TypeStatisticsGraph; followers: StatisticsOverviewItem; viewsPerPost: StatisticsOverviewItem; sharesPerPost: StatisticsOverviewItem; @@ -25,19 +26,20 @@ export interface ApiChannelStatistics { } export interface ApiChannelMonetizationStatistics { - topHoursGraph?: StatisticsGraph | string; - revenueGraph?: StatisticsGraph | string; + topHoursGraph?: TypeStatisticsGraph; + revenueGraph?: TypeStatisticsGraph; balances?: ChannelMonetizationBalances; usdRate?: number; } export interface ApiGroupStatistics { - growthGraph?: StatisticsGraph | string; - membersGraph?: StatisticsGraph | string; - topHoursGraph?: StatisticsGraph | string; - languagesGraph: StatisticsGraph | string; - messagesGraph: StatisticsGraph | string; - actionsGraph: StatisticsGraph | string; + type: 'group'; + growthGraph?: TypeStatisticsGraph; + membersGraph?: TypeStatisticsGraph; + topHoursGraph?: TypeStatisticsGraph; + languagesGraph: TypeStatisticsGraph; + messagesGraph: TypeStatisticsGraph; + actionsGraph: TypeStatisticsGraph; period: StatisticsOverviewPeriod; members: StatisticsOverviewItem; viewers: StatisticsOverviewItem; @@ -46,8 +48,8 @@ export interface ApiGroupStatistics { } export interface ApiPostStatistics { - viewsGraph?: StatisticsGraph | string; - reactionsGraph?: StatisticsGraph | string; + viewsGraph?: TypeStatisticsGraph; + reactionsGraph?: TypeStatisticsGraph; forwardsCount?: number; viewsCount?: number; reactionsCount?: number; @@ -80,6 +82,7 @@ export interface ApiStoryPublicForward { } export interface StatisticsGraph { + graphType: 'graph'; type: string; zoomToken?: string; labelFormatter: string; @@ -104,6 +107,18 @@ export interface StatisticsGraph { }; } +export interface StatisticsGraphError { + graphType: 'error'; + error: string; +} + +export interface StatisticsGraphAsync { + graphType: 'async'; + token: string; +} + +export type TypeStatisticsGraph = StatisticsGraph | StatisticsGraphError | StatisticsGraphAsync; + export interface StatisticsOverviewItem { current?: number; change?: number; @@ -122,6 +137,7 @@ export interface StatisticsOverviewPeriod { } export interface StatisticsMessageInteractionCounter { + type: 'message'; msgId: number; forwardsCount: number; viewsCount: number; @@ -129,6 +145,7 @@ export interface StatisticsMessageInteractionCounter { } export interface StatisticsStoryInteractionCounter { + type: 'story'; storyId: number; viewsCount: number; forwardsCount: number; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index b7eba391a..1861ec5f4 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -1026,7 +1026,11 @@ const Message: FC = ({ return (
diff --git a/src/components/right/statistics/MessageStatistics.tsx b/src/components/right/statistics/MessageStatistics.tsx index 568bb48c8..a281c9989 100644 --- a/src/components/right/statistics/MessageStatistics.tsx +++ b/src/components/right/statistics/MessageStatistics.tsx @@ -6,14 +6,13 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiMessagePublicForward, ApiPostStatistics, - StatisticsGraph, } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; -import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config'; import { selectChatFullInfo, selectTabState } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { callApi } from '../../../api/gramjs'; +import { isGraph } from './helpers/isGraph'; import useForceUpdate from '../../../hooks/useForceUpdate'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -56,7 +55,7 @@ export type StateProps = { dcId?: number; }; -function Statistics({ +function MessageStatistics({ chatId, isActive, statistics, @@ -66,7 +65,8 @@ function Statistics({ const lang = useOldLang(); const containerRef = useRef(); const [isReady, setIsReady] = useState(false); - const loadedCharts = useRef([]); + const loadedCharts = useRef>(new Set()); + const errorCharts = useRef>(new Set()); const { loadMessageStatistics, loadMessagePublicForwards, loadStatisticsAsyncGraph } = getActions(); const forceUpdate = useForceUpdate(); @@ -79,7 +79,8 @@ function Statistics({ useEffect(() => { if (!isActive || messageId) { - loadedCharts.current = []; + loadedCharts.current.clear(); + errorCharts.current.clear(); setIsReady(false); } }, [isActive, messageId]); @@ -92,10 +93,13 @@ function Statistics({ GRAPHS.forEach((name) => { const graph = statistics[name]; - const isAsync = typeof graph === 'string'; + if (!isGraph(graph)) { + return; + } + const isAsync = graph.graphType === 'async'; if (isAsync) { - loadStatisticsAsyncGraph({ name, chatId, token: graph }); + loadStatisticsAsyncGraph({ name, chatId, token: graph.token }); } }); }, [chatId, statistics, loadStatisticsAsyncGraph]); @@ -115,19 +119,24 @@ function Statistics({ GRAPHS.forEach((name, index: number) => { const graph = statistics[name]; - const isAsync = typeof graph === 'string'; + if (!isGraph(graph)) { + return; + } + const isAsync = graph.graphType === 'async'; + const isError = graph.graphType === 'error'; - if (isAsync || loadedCharts.current.includes(name)) { + if (isAsync || loadedCharts.current.has(name)) { return; } - if (!graph) { - loadedCharts.current.push(name); + if (isError) { + loadedCharts.current.add(name); + errorCharts.current.add(name); return; } - const { zoomToken } = graph as StatisticsGraph; + const { zoomToken } = graph; LovelyChart.create( containerRef.current!.children[index] as HTMLElement, @@ -137,11 +146,11 @@ function Statistics({ onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }), zoomOutLabel: lang('Graph.ZoomOut'), } : {}, - ...graph as StatisticsGraph, + ...graph, }, ); - loadedCharts.current.push(name); + loadedCharts.current.add(name); }); forceUpdate(); @@ -161,15 +170,21 @@ function Statistics({ } return ( -
+
- {!loadedCharts.current.length && } + {(!loadedCharts.current.size || !statistics.publicForwardsData) && }
- {GRAPHS.map((graph) => ( -
- ))} + {GRAPHS.map((graph) => { + const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); + return ( +
+ ); + })}
{Boolean(statistics.publicForwards) && ( @@ -180,7 +195,6 @@ function Statistics({ items={statistics.publicForwardsData} itemSelector=".statistic-public-forward" onLoadMore={handleLoadMore} - preloadBackwards={STATISTICS_PUBLIC_FORWARDS_LIMIT} noFastList > {(statistics.publicForwardsData as ApiMessagePublicForward[]).map((item) => ( @@ -202,4 +216,4 @@ export default memo(withGlobal( return { statistics, dcId, messageId }; }, -)(Statistics)); +)(MessageStatistics)); diff --git a/src/components/right/statistics/MonetizationStatistics.tsx b/src/components/right/statistics/MonetizationStatistics.tsx index 12a52f824..83ffbebe5 100644 --- a/src/components/right/statistics/MonetizationStatistics.tsx +++ b/src/components/right/statistics/MonetizationStatistics.tsx @@ -3,11 +3,12 @@ import { } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ApiChannelMonetizationStatistics, StatisticsGraph } from '../../../api/types'; +import type { ApiChannelMonetizationStatistics } from '../../../api/types'; import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import renderText from '../../common/helpers/renderText'; +import { isGraph } from './helpers/isGraph'; import useFlag from '../../../hooks/useFlag'; import useForceUpdate from '../../../hooks/useForceUpdate'; @@ -68,7 +69,9 @@ const MonetizationStatistics = ({ const containerRef = useRef(); const [isReady, setIsReady] = useState(false); - const loadedCharts = useRef([]); + const loadedCharts = useRef>(new Set()); + const errorCharts = useRef>(new Set()); + const forceUpdate = useForceUpdate(); const [isAboutMonetizationModalOpen, openAboutMonetizationModal, closeAboutMonetizationModal] = useFlag(false); const [isConfirmPasswordDialogOpen, openConfirmPasswordDialogOpen, closeConfirmPasswordDialogOpen] = useFlag(); @@ -100,7 +103,8 @@ const MonetizationStatistics = ({ }); } - loadedCharts.current = []; + loadedCharts.current.clear(); + errorCharts.current.clear(); if (!statistics || !containerRef.current) { return; @@ -108,24 +112,29 @@ const MonetizationStatistics = ({ MONETIZATION_GRAPHS.forEach((name, index: number) => { const graph = statistics[name]; - const isAsync = typeof graph === 'string'; + if (!isGraph(graph)) { + return; + } + const isAsync = graph.graphType === 'async'; + const isError = graph.graphType === 'error'; - if (isAsync || loadedCharts.current.includes(name)) { + if (isAsync || loadedCharts.current.has(name)) { return; } - if (!graph) { - loadedCharts.current.push(name); + if (isError) { + loadedCharts.current.add(name); + errorCharts.current.add(name); return; } LovelyChart.create(containerRef.current!.children[index] as HTMLElement, { title: oldLang((MONETIZATION_GRAPHS_TITLES as Record)[name]), - ...graph as StatisticsGraph, + ...graph, }); - loadedCharts.current.push(name); + loadedCharts.current.add(name); containerRef.current!.children[index].classList.remove(styles.hidden); }); @@ -232,7 +241,7 @@ const MonetizationStatistics = ({ } /> - {!loadedCharts.current.length && } + {!loadedCharts.current.size && }
{MONETIZATION_GRAPHS.filter(Boolean).map((graph) => ( diff --git a/src/components/right/statistics/Statistics.module.scss b/src/components/right/statistics/Statistics.module.scss index 25031a78d..1ff01960b 100644 --- a/src/components/right/statistics/Statistics.module.scss +++ b/src/components/right/statistics/Statistics.module.scss @@ -36,7 +36,6 @@ .messages, .publicForwards { padding: 1rem 0; - border-top: 1px solid var(--color-borders); &-title { padding-left: 0.75rem; @@ -69,6 +68,7 @@ &.hidden { margin: 0; + border-bottom: none; opacity: 0; } } diff --git a/src/components/right/statistics/Statistics.tsx b/src/components/right/statistics/Statistics.tsx index 5a8c7673c..22926438a 100644 --- a/src/components/right/statistics/Statistics.tsx +++ b/src/components/right/statistics/Statistics.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../../lib/teact/teact'; import { memo, useEffect, useMemo, useRef, useState, @@ -22,6 +21,7 @@ import { } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { callApi } from '../../../api/gramjs'; +import { isGraph } from './helpers/isGraph'; import useForceUpdate from '../../../hooks/useForceUpdate'; import useOldLang from '../../../hooks/useOldLang'; @@ -84,7 +84,7 @@ export type StateProps = { storiesById?: Record; }; -const Statistics: FC = ({ +const Statistics = ({ chatId, chat, statistics, @@ -92,11 +92,12 @@ const Statistics: FC = ({ isGroup, messagesById, storiesById, -}) => { +}: OwnProps & StateProps) => { const lang = useOldLang(); const containerRef = useRef(); const [isReady, setIsReady] = useState(false); - const loadedCharts = useRef([]); + const loadedCharts = useRef>(new Set()); + const errorCharts = useRef>(new Set()); const { loadStatistics, loadStatisticsAsyncGraph } = getActions(); const forceUpdate = useForceUpdate(); @@ -121,13 +122,16 @@ const Statistics: FC = ({ graphs.forEach((name) => { const graph = statistics[name as keyof typeof statistics]; - const isAsync = typeof graph === 'string'; + if (!isGraph(graph)) { + return; + } + const isAsync = graph.graphType === 'async'; if (isAsync) { loadStatisticsAsyncGraph({ name, chatId, - token: graph, + token: graph.token, // Hardcode percentage for languages graph, since API does not return `percentage` flag isPercentage: name === 'languagesGraph', }); @@ -150,14 +154,20 @@ const Statistics: FC = ({ graphs.forEach((name, index: number) => { const graph = statistics[name as keyof typeof statistics]; - const isAsync = typeof graph === 'string'; - - if (isAsync || loadedCharts.current.includes(name)) { + if (!isGraph(graph)) { return; } - if (!graph) { - loadedCharts.current.push(name); + const isAsync = graph.graphType === 'async'; + const isError = graph.graphType === 'error'; + + if (isAsync || loadedCharts.current.has(name)) { + return; + } + + if (isError) { + loadedCharts.current.add(name); + errorCharts.current.add(name); return; } @@ -176,7 +186,7 @@ const Statistics: FC = ({ }, ); - loadedCharts.current.push(name); + loadedCharts.current.add(name); containerRef.current!.children[index].classList.remove(styles.hidden); }); @@ -197,12 +207,15 @@ const Statistics: FC = ({ /> )} - {!loadedCharts.current.length && } + {!loadedCharts.current.size && }
- {graphs.map((graph) => ( -
- ))} + {graphs.map((graph) => { + const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); + return ( +
+ ); + })}
{Boolean((statistics as ApiChannelStatistics)?.recentPosts?.length) && ( diff --git a/src/components/right/statistics/StatisticsOverview.tsx b/src/components/right/statistics/StatisticsOverview.tsx index 873cea874..7e1dbe36d 100644 --- a/src/components/right/statistics/StatisticsOverview.tsx +++ b/src/components/right/statistics/StatisticsOverview.tsx @@ -100,6 +100,8 @@ const BOOST_OVERVIEW: OverviewCell[][] = [ type StatisticsType = 'channel' | 'group' | 'message' | 'boost' | 'story' | 'monetization'; +const DEFAULT_VALUE = 0; + export type OwnProps = { type: StatisticsType; title?: string; @@ -212,13 +214,13 @@ const StatisticsOverview: FC = ({ ) : schema.map((row) => ( {row.map((cell: OverviewCell) => { - const field = (statistics as any)[cell.name]; + const field = (statistics as any)?.[cell.name]; if (cell.isPlain) { return ( - {`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`} + {`${cell.isApproximate ? '≈ ' : ''}${formatInteger(field ?? DEFAULT_VALUE)}`}

{oldLang(cell.title)}

@@ -226,15 +228,18 @@ const StatisticsOverview: FC = ({ } if (cell.isPercentage) { + const part = field?.part ?? DEFAULT_VALUE; + const percentage = field?.percentage ?? DEFAULT_VALUE; + return ( {cell.withAbsoluteValue && ( - {`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`} + {`${cell.isApproximate ? '≈ ' : ''}${formatInteger(part)}`} )} - {field.percentage} + {percentage} %

{oldLang(cell.title)}

@@ -245,7 +250,7 @@ const StatisticsOverview: FC = ({ return ( - {formatIntegerCompact(lang, field.current)} + {formatIntegerCompact(lang, field?.current ?? DEFAULT_VALUE)} {' '} {renderOverviewItemValue(field)} diff --git a/src/components/right/statistics/StoryStatistics.tsx b/src/components/right/statistics/StoryStatistics.tsx index 74f9907bd..ec63a6604 100644 --- a/src/components/right/statistics/StoryStatistics.tsx +++ b/src/components/right/statistics/StoryStatistics.tsx @@ -7,13 +7,12 @@ import type { ApiChat, ApiPostStatistics, ApiUser, - StatisticsGraph, } from '../../../api/types'; -import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config'; import { selectChatFullInfo, selectTabState } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { callApi } from '../../../api/gramjs'; +import { isGraph } from './helpers/isGraph'; import useForceUpdate from '../../../hooks/useForceUpdate'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -71,7 +70,8 @@ function StoryStatistics({ const lang = useOldLang(); const containerRef = useRef(); const [isReady, setIsReady] = useState(false); - const loadedCharts = useRef([]); + const loadedCharts = useRef>(new Set()); + const errorCharts = useRef>(new Set()); const { loadStoryStatistics, loadStoryPublicForwards, loadStatisticsAsyncGraph } = getActions(); const forceUpdate = useForceUpdate(); @@ -84,7 +84,8 @@ function StoryStatistics({ useEffect(() => { if (!isActive || storyId) { - loadedCharts.current = []; + loadedCharts.current.clear(); + errorCharts.current.clear(); setIsReady(false); } }, [isActive, storyId]); @@ -97,10 +98,13 @@ function StoryStatistics({ GRAPHS.forEach((name) => { const graph = statistics[name]; - const isAsync = typeof graph === 'string'; + if (!isGraph(graph)) { + return; + } + const isAsync = graph.graphType === 'async'; if (isAsync) { - loadStatisticsAsyncGraph({ name, chatId, token: graph }); + loadStatisticsAsyncGraph({ name, chatId, token: graph.token }); } }); }, [chatId, statistics, loadStatisticsAsyncGraph]); @@ -120,19 +124,24 @@ function StoryStatistics({ GRAPHS.forEach((name, index: number) => { const graph = statistics[name]; - const isAsync = typeof graph === 'string'; + if (!isGraph(graph)) { + return; + } + const isAsync = graph.graphType === 'async'; + const isError = graph.graphType === 'error'; - if (isAsync || loadedCharts.current.includes(name)) { + if (isAsync || loadedCharts.current.has(name)) { return; } - if (!graph) { - loadedCharts.current.push(name); + if (isError) { + loadedCharts.current.add(name); + errorCharts.current.add(name); return; } - const { zoomToken } = graph as StatisticsGraph; + const { zoomToken } = graph; LovelyChart.create( containerRef.current!.children[index] as HTMLElement, @@ -142,11 +151,11 @@ function StoryStatistics({ onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }), zoomOutLabel: lang('Graph.ZoomOut'), } : {}, - ...graph as StatisticsGraph, + ...graph, }, ); - loadedCharts.current.push(name); + loadedCharts.current.add(name); }); forceUpdate(); @@ -166,15 +175,21 @@ function StoryStatistics({ } return ( -
+
- {!loadedCharts.current.length && } + {!loadedCharts.current.size && }
- {GRAPHS.map((graph) => ( -
- ))} + {GRAPHS.map((graph) => { + const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); + return ( +
+ ); + })}
{Boolean(statistics.publicForwards) && ( @@ -185,7 +200,6 @@ function StoryStatistics({ items={statistics.publicForwardsData} itemSelector=".statistic-public-forward" onLoadMore={handleLoadMore} - preloadBackwards={STATISTICS_PUBLIC_FORWARDS_LIMIT} noFastList > {statistics.publicForwardsData!.map((item) => { diff --git a/src/components/right/statistics/helpers/isGraph.ts b/src/components/right/statistics/helpers/isGraph.ts new file mode 100644 index 000000000..2c3487293 --- /dev/null +++ b/src/components/right/statistics/helpers/isGraph.ts @@ -0,0 +1,6 @@ +import type { TypeStatisticsGraph } from '../../../../api/types'; + +export function isGraph(obj: unknown): obj is TypeStatisticsGraph { + // eslint-disable-next-line no-null/no-null + return typeof obj === 'object' && obj !== null && 'graphType' in obj; +} diff --git a/src/config.ts b/src/config.ts index 76433ddab..ec6b0279f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -104,7 +104,6 @@ export const TOPIC_LIST_SENSITIVE_AREA = 600; export const GROUP_CALL_PARTICIPANTS_LIMIT = 100; export const STORY_LIST_LIMIT = 100; export const API_GENERAL_ID_LIMIT = 100; -export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50; export const RESALE_GIFTS_LIMIT = 50; export const TODO_ITEMS_LIMIT = 30; export const TODO_TITLE_LENGTH_LIMIT = 32; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index d14743446..a69d50732 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -314,6 +314,24 @@ addActionHandler('loadMessage', async (global, actions, payload): Promise } }); +addActionHandler('loadMessagesById', async (global, actions, payload): Promise => { + const { chatId, messageIds } = payload; + const chat = selectChat(global, chatId); + if (!chat) { + return; + } + + const messages = await callApi('fetchMessagesById', { + chat, + messageIds, + }); + if (!messages) return; + + global = getGlobal(); + global = addChatMessagesById(global, chatId, buildCollectionByKey(messages, 'id')); + setGlobal(global); +}); + addActionHandler('sendMessage', async (global, actions, payload): Promise => { const { messageList, tabId = getCurrentTabId() } = payload; diff --git a/src/global/actions/api/statistics.ts b/src/global/actions/api/statistics.ts index 3cb37f910..eea2c38dd 100644 --- a/src/global/actions/api/statistics.ts +++ b/src/global/actions/api/statistics.ts @@ -1,4 +1,3 @@ -import { areDeepEqual } from '../../../util/areDeepEqual'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { callApi } from '../../../api/gramjs'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; @@ -39,6 +38,25 @@ addActionHandler('loadStatistics', async (global, actions, payload): Promise post.type === 'message'); + const storyInteractions = stats.recentPosts.filter((post) => post.type === 'story'); + + if (messageInteractions.length > 0) { + actions.loadMessagesById({ + chatId, + messageIds: messageInteractions.map((interaction) => interaction.msgId), + }); + } + + if (storyInteractions.length > 0) { + actions.loadPeerStoriesByIds({ + peerId: chatId, + storyIds: storyInteractions.map((interaction) => interaction.storyId), + }); + } + } }); addActionHandler('loadChannelMonetizationStatistics', async (global, actions, payload): Promise => { @@ -122,17 +140,11 @@ addActionHandler('loadMessagePublicForwards', async (global, actions, payload): count, } = publicForwards || {}; - // Api returns the last element from the previous page as the first element - const shouldOmitFirstElement = stats.publicForwardsData?.length && forwards?.length - && areDeepEqual(stats.publicForwardsData[stats.publicForwardsData.length - 1], forwards[0]); - global = getGlobal(); global = updateMessageStatistics(global, { ...stats, publicForwards: count || forwards?.length, - publicForwardsData: (stats.publicForwardsData || []).concat( - shouldOmitFirstElement ? forwards.slice(1) : (forwards || []), - ), + publicForwardsData: (stats.publicForwardsData || []).concat((forwards || [])), nextOffset, }, tabId); setGlobal(global); diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index b5dfe5cde..373a42c41 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -184,7 +184,9 @@ addActionHandler('toggleMessageStatistics', (global, actions, payload): ActionRe statistics: { ...selectTabState(global, tabId).statistics, currentMessageId: messageId, + currentMessage: undefined, currentStoryId: undefined, + currentStory: undefined, }, }, tabId); }); @@ -196,6 +198,8 @@ addActionHandler('toggleStoryStatistics', (global, actions, payload): ActionRetu ...selectTabState(global, tabId).statistics, currentStoryId: storyId, currentMessageId: undefined, + currentMessage: undefined, + currentStory: undefined, }, }, tabId); }); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 5a76942d6..9ad552179 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -504,6 +504,10 @@ export interface ActionPayloads { isDeleting?: boolean; }; }; + loadMessagesById: { + chatId: string; + messageIds: number[]; + }; editMessage: { messageList?: MessageList; text: string; @@ -1570,7 +1574,7 @@ export interface ActionPayloads { loadPeerStoriesByIds: { peerId: string; storyIds: number[]; - } & WithTabId; + }; viewStory: { peerId: string; storyId: number;