2023-10-27 12:52:06 +02:00

220 lines
6.3 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo,
useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiChannelStatistics,
ApiGroupStatistics,
ApiMessage,
StatisticsGraph,
StatisticsRecentMessage as StatisticsRecentMessageType,
} from '../../../api/types';
import { selectChat, selectChatFullInfo, selectStatistics } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { callApi } from '../../../api/gramjs';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useLang from '../../../hooks/useLang';
import Loading from '../../ui/Loading';
import StatisticsOverview from './StatisticsOverview';
import StatisticsRecentMessage from './StatisticsRecentMessage';
import './Statistics.scss';
type ILovelyChart = { create: Function };
let lovelyChartPromise: Promise<ILovelyChart>;
let LovelyChart: ILovelyChart;
async function ensureLovelyChart() {
if (!lovelyChartPromise) {
lovelyChartPromise = import('../../../lib/lovely-chart/LovelyChart') as Promise<ILovelyChart>;
LovelyChart = await lovelyChartPromise;
}
return lovelyChartPromise;
}
const CHANNEL_GRAPHS_TITLES = {
growthGraph: 'ChannelStats.Graph.Growth',
followersGraph: 'ChannelStats.Graph.Followers',
muteGraph: 'ChannelStats.Graph.Notifications',
topHoursGraph: 'ChannelStats.Graph.ViewsByHours',
viewsBySourceGraph: 'ChannelStats.Graph.ViewsBySource',
newFollowersBySourceGraph: 'ChannelStats.Graph.NewFollowersBySource',
languagesGraph: 'ChannelStats.Graph.Language',
interactionsGraph: 'ChannelStats.Graph.Interactions',
};
const CHANNEL_GRAPHS = Object.keys(CHANNEL_GRAPHS_TITLES) as (keyof ApiChannelStatistics)[];
const GROUP_GRAPHS_TITLES = {
growthGraph: 'Stats.GroupGrowthTitle',
membersGraph: 'Stats.GroupMembersTitle',
languagesGraph: 'Stats.GroupLanguagesTitle',
messagesGraph: 'Stats.GroupMessagesTitle',
actionsGraph: 'Stats.GroupActionsTitle',
topHoursGraph: 'Stats.GroupTopHoursTitle',
};
const GROUP_GRAPHS = Object.keys(GROUP_GRAPHS_TITLES) as (keyof ApiGroupStatistics)[];
export type OwnProps = {
chatId: string;
};
export type StateProps = {
statistics: ApiChannelStatistics | ApiGroupStatistics;
dcId?: number;
isGroup: boolean;
};
const Statistics: FC<OwnProps & StateProps> = ({
chatId,
statistics,
dcId,
isGroup,
}) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const [isReady, setIsReady] = useState(false);
const loadedCharts = useRef<string[]>([]);
const { loadStatistics, loadStatisticsAsyncGraph } = getActions();
const forceUpdate = useForceUpdate();
useEffect(() => {
loadStatistics({ chatId, isGroup });
}, [chatId, loadStatistics, isGroup]);
const graphs = useMemo(() => {
return isGroup ? GROUP_GRAPHS : CHANNEL_GRAPHS;
}, [isGroup]);
const graphTitles = useMemo(() => {
return isGroup ? GROUP_GRAPHS_TITLES : CHANNEL_GRAPHS_TITLES;
}, [isGroup]);
// Load async graphs
useEffect(() => {
if (!statistics) {
return;
}
graphs.forEach((name) => {
const graph = statistics[name as keyof typeof statistics];
const isAsync = typeof graph === 'string';
if (isAsync) {
loadStatisticsAsyncGraph({
name,
chatId,
token: graph,
// Hardcode percentage for languages graph, since API does not return `percentage` flag
isPercentage: name === 'languagesGraph',
});
}
});
}, [graphs, chatId, statistics, loadStatisticsAsyncGraph]);
useEffect(() => {
(async () => {
await ensureLovelyChart();
if (!isReady) {
setIsReady(true);
return;
}
if (!statistics || !containerRef.current) {
return;
}
graphs.forEach((name, index: number) => {
const graph = statistics[name as keyof typeof statistics];
const isAsync = typeof graph === 'string';
if (isAsync || loadedCharts.current.includes(name)) {
return;
}
if (!graph) {
loadedCharts.current.push(name);
return;
}
const { zoomToken } = graph;
LovelyChart.create(
containerRef.current!.children[index],
{
title: lang((graphTitles as Record<string, string>)[name]),
...zoomToken ? {
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
zoomOutLabel: lang('Graph.ZoomOut'),
} : {},
...graph as StatisticsGraph,
},
);
loadedCharts.current.push(name);
containerRef.current!.children[index].classList.remove('hidden');
});
forceUpdate();
})();
}, [
graphs, graphTitles, isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId, forceUpdate,
]);
if (!isReady || !statistics) {
return <Loading />;
}
return (
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
<StatisticsOverview
statistics={statistics}
type={isGroup ? 'group' : 'channel'}
title={lang('StatisticOverview')}
/>
{!loadedCharts.current.length && <Loading />}
<div ref={containerRef}>
{graphs.map((graph) => (
<div key={graph} className="Statistics__graph hidden" />
))}
</div>
{Boolean((statistics as ApiChannelStatistics).recentTopMessages?.length) && (
<div className="Statistics__messages">
<h2 className="Statistics__messages-title">{lang('ChannelStats.Recent.Header')}</h2>
{(statistics as ApiChannelStatistics).recentTopMessages.map((message) => (
<StatisticsRecentMessage message={message as ApiMessage & StatisticsRecentMessageType} />
))}
</div>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const statistics = selectStatistics(global, chatId);
const chat = selectChat(global, chatId);
const dcId = selectChatFullInfo(global, chatId)?.statisticsDcId;
const isGroup = chat?.type === 'chatTypeSuperGroup';
return {
statistics, dcId, isGroup,
};
},
)(Statistics));