import { memo, useEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiChannelStatistics, ApiChat, ApiGroupStatistics, ApiMessage, ApiTypeStory, } from '../../../api/types'; import { selectChat, selectChatFullInfo, selectChatMessages, selectPeerStories, selectStatistics, } 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'; import Loading from '../../ui/Loading'; import StatisticsOverview from './StatisticsOverview'; import StatisticsRecentMessage from './StatisticsRecentMessage'; import StatisticsRecentStory from './StatisticsRecentStory'; import styles from './Statistics.module.scss'; type ILovelyChart = { create: (el: HTMLElement, params: AnyLiteral) => void }; let lovelyChartPromise: Promise | undefined; let LovelyChart: ILovelyChart; async function ensureLovelyChart() { if (!lovelyChartPromise) { lovelyChartPromise = import('../../../lib/lovely-chart/LovelyChart') as Promise; 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', reactionsByEmotionGraph: 'ChannelStats.Graph.Reactions', storyInteractionsGraph: 'ChannelStats.Graph.Stories', storyReactionsByEmotionGraph: 'ChannelStats.Graph.StoriesReactions', }; 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 = { chat?: ApiChat; statistics: ApiChannelStatistics | ApiGroupStatistics; dcId?: number; isGroup: boolean; messagesById: Record; storiesById?: Record; }; const Statistics = ({ chatId, chat, statistics, dcId, isGroup, messagesById, storiesById, }: OwnProps & StateProps) => { const lang = useOldLang(); const containerRef = useRef(); const [isReady, setIsReady] = useState(false); const loadedCharts = useRef>(new Set()); const errorCharts = useRef>(new Set()); 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]; if (!isGraph(graph)) { return; } const isAsync = graph.graphType === 'async'; if (isAsync) { loadStatisticsAsyncGraph({ name, chatId, token: graph.token, // 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]; if (!isGraph(graph)) { return; } 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; } const { zoomToken } = graph; LovelyChart.create( containerRef.current!.children[index] as HTMLElement, { title: lang((graphTitles as Record)[name]), ...zoomToken ? { onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }), zoomOutLabel: lang('Graph.ZoomOut'), } : {}, ...graph, }, ); loadedCharts.current.add(name); containerRef.current!.children[index].classList.remove(styles.hidden); }); forceUpdate(); })(); }, [ graphs, graphTitles, isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId, forceUpdate, ]); return (
{statistics && ( )} {!loadedCharts.current.size && }
{graphs.map((graph) => { const isGraphReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph); return (
); })}
{Boolean((statistics as ApiChannelStatistics)?.recentPosts?.length) && (

{lang('ChannelStats.Recent.Header')}

{(statistics as ApiChannelStatistics).recentPosts.map((postStatistic) => { if ('msgId' in postStatistic) { const message = messagesById[postStatistic.msgId]; if (!message || !('content' in message)) return undefined; return ( ); } if ('storyId' in postStatistic && chat) { const story = storiesById?.[postStatistic.storyId]; return ( ); } return undefined; })}
)}
); }; export default memo(withGlobal( (global, { chatId }): StateProps => { const statistics = selectStatistics(global, chatId); const chat = selectChat(global, chatId); const dcId = selectChatFullInfo(global, chatId)?.statisticsDcId; const isGroup = chat?.type === 'chatTypeSuperGroup'; const messagesById = selectChatMessages(global, chatId); const storiesById = selectPeerStories(global, chatId)?.byId; return { statistics, dcId, isGroup, chat, messagesById, storiesById, }; }, )(Statistics));