Statistics: Fix statistics overview (#5963)

This commit is contained in:
Alexander Zinchuk 2025-08-21 12:05:42 +02:00
parent f4240a93ab
commit fb79e088fe
17 changed files with 288 additions and 128 deletions

View File

@ -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),

View File

@ -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(

View File

@ -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,
});

View File

@ -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;

View File

@ -1026,7 +1026,11 @@ const Message: FC<OwnProps & StateProps> = ({
return (
<div
className={buildClassName('quick-reaction', isQuickReactionVisible && !hasActiveReactions && 'visible')}
className={buildClassName(
'quick-reaction',
'no-selection',
isQuickReactionVisible && !hasActiveReactions && 'visible',
)}
onClick={handleSendQuickReaction}
ref={quickReactionRef}
>

View File

@ -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<HTMLDivElement>();
const [isReady, setIsReady] = useState(false);
const loadedCharts = useRef<string[]>([]);
const loadedCharts = useRef<Set<string>>(new Set());
const errorCharts = useRef<Set<string>>(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 (
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
<div
key={`${chatId}-${messageId}`}
className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}
>
<StatisticsOverview statistics={statistics} type="message" title={lang('StatisticOverview')} />
{!loadedCharts.current.length && <Loading />}
{(!loadedCharts.current.size || !statistics.publicForwardsData) && <Loading />}
<div ref={containerRef}>
{GRAPHS.map((graph) => (
<div className={buildClassName(styles.graph, !loadedCharts.current.includes(graph) && styles.hidden)} />
))}
{GRAPHS.map((graph) => {
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
return (
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} />
);
})}
</div>
{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<OwnProps>(
return { statistics, dcId, messageId };
},
)(Statistics));
)(MessageStatistics));

View File

@ -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<HTMLDivElement>();
const [isReady, setIsReady] = useState(false);
const loadedCharts = useRef<string[]>([]);
const loadedCharts = useRef<Set<string>>(new Set());
const errorCharts = useRef<Set<string>>(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<string, string>)[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 && <Loading />}
{!loadedCharts.current.size && <Loading />}
<div ref={containerRef} className={styles.section}>
{MONETIZATION_GRAPHS.filter(Boolean).map((graph) => (

View File

@ -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;
}
}

View File

@ -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<string, ApiTypeStory>;
};
const Statistics: FC<OwnProps & StateProps> = ({
const Statistics = ({
chatId,
chat,
statistics,
@ -92,11 +92,12 @@ const Statistics: FC<OwnProps & StateProps> = ({
isGroup,
messagesById,
storiesById,
}) => {
}: OwnProps & StateProps) => {
const lang = useOldLang();
const containerRef = useRef<HTMLDivElement>();
const [isReady, setIsReady] = useState(false);
const loadedCharts = useRef<string[]>([]);
const loadedCharts = useRef<Set<string>>(new Set());
const errorCharts = useRef<Set<string>>(new Set());
const { loadStatistics, loadStatisticsAsyncGraph } = getActions();
const forceUpdate = useForceUpdate();
@ -121,13 +122,16 @@ const Statistics: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
},
);
loadedCharts.current.push(name);
loadedCharts.current.add(name);
containerRef.current!.children[index].classList.remove(styles.hidden);
});
@ -197,12 +207,15 @@ const Statistics: FC<OwnProps & StateProps> = ({
/>
)}
{!loadedCharts.current.length && <Loading />}
{!loadedCharts.current.size && <Loading />}
<div ref={containerRef}>
{graphs.map((graph) => (
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
))}
{graphs.map((graph) => {
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
return (
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} />
);
})}
</div>
{Boolean((statistics as ApiChannelStatistics)?.recentPosts?.length) && (

View File

@ -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<OwnProps> = ({
) : schema.map((row) => (
<tr>
{row.map((cell: OverviewCell) => {
const field = (statistics as any)[cell.name];
const field = (statistics as any)?.[cell.name];
if (cell.isPlain) {
return (
<td className={styles.tableCell}>
<b className={styles.tableValue}>
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
{`${cell.isApproximate ? '≈ ' : ''}${formatInteger(field ?? DEFAULT_VALUE)}`}
</b>
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
</td>
@ -226,15 +228,18 @@ const StatisticsOverview: FC<OwnProps> = ({
}
if (cell.isPercentage) {
const part = field?.part ?? DEFAULT_VALUE;
const percentage = field?.percentage ?? DEFAULT_VALUE;
return (
<td className={styles.tableCell}>
{cell.withAbsoluteValue && (
<span className={styles.tableValue}>
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`}
{`${cell.isApproximate ? '≈ ' : ''}${formatInteger(part)}`}
</span>
)}
<span className={cell.withAbsoluteValue ? styles.tableSecondaryValue : styles.tableValue}>
{field.percentage}
{percentage}
%
</span>
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
@ -245,7 +250,7 @@ const StatisticsOverview: FC<OwnProps> = ({
return (
<td className={styles.tableCell}>
<b className={styles.tableValue}>
{formatIntegerCompact(lang, field.current)}
{formatIntegerCompact(lang, field?.current ?? DEFAULT_VALUE)}
</b>
{' '}
{renderOverviewItemValue(field)}

View File

@ -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<HTMLDivElement>();
const [isReady, setIsReady] = useState(false);
const loadedCharts = useRef<string[]>([]);
const loadedCharts = useRef<Set<string>>(new Set());
const errorCharts = useRef<Set<string>>(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 (
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
<div
key={`${chatId}-${storyId}`}
className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}
>
<StatisticsOverview statistics={statistics} type="story" title={lang('StatisticOverview')} />
{!loadedCharts.current.length && <Loading />}
{!loadedCharts.current.size && <Loading />}
<div ref={containerRef}>
{GRAPHS.map((graph) => (
<div className={buildClassName(styles.graph, !loadedCharts.current.includes(graph) && styles.hidden)} />
))}
{GRAPHS.map((graph) => {
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
return (
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} />
);
})}
</div>
{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) => {

View File

@ -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;
}

View File

@ -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;

View File

@ -314,6 +314,24 @@ addActionHandler('loadMessage', async (global, actions, payload): Promise<void>
}
});
addActionHandler('loadMessagesById', async (global, actions, payload): Promise<void> => {
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<void> => {
const { messageList, tabId = getCurrentTabId() } = payload;

View File

@ -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<voi
global = getGlobal();
global = updateStatistics(global, chatId, stats, tabId);
setGlobal(global);
if (stats.type === 'channel') {
const messageInteractions = stats.recentPosts.filter((post) => 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<void> => {
@ -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);

View File

@ -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);
});

View File

@ -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;