220 lines
6.3 KiB
TypeScript
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,
|
|
StatisticsMessageInteractionCounter,
|
|
} 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 & StatisticsMessageInteractionCounter} />
|
|
))}
|
|
</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));
|