Statistics: Posts and messages (#1801)

This commit is contained in:
Alexander Zinchuk 2022-04-19 15:12:26 +02:00
parent c3318d93e6
commit 47a26ab9f1
29 changed files with 410 additions and 43 deletions

View File

@ -137,7 +137,7 @@ type UniversalMessage = (
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions'
'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' | 'forwards'
)>
);
@ -172,6 +172,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
date: mtpMessage.date,
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
views: mtpMessage.views,
forwards: mtpMessage.forwards,
isFromScheduled: mtpMessage.fromScheduled,
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
...(replyToMsgId && { replyToMessageId: replyToMsgId }),

View File

@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import {
ApiChannelStatistics,
ApiGroupStatistics,
ApiMessageStatistics,
StatisticsGraph,
StatisticsOverviewItem,
StatisticsOverviewPercentage,
@ -54,9 +55,17 @@ export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGro
};
}
export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean): StatisticsGraph {
export function buildMessageStatistics(stats: GramJs.stats.MessageStats): ApiMessageStatistics {
return {
viewsGraph: buildGraph(stats.viewsGraph),
};
}
export function buildGraph(
result: GramJs.TypeStatsGraph, isPercentage?: boolean
): StatisticsGraph | undefined {
if ((result as GramJs.StatsGraphError).error) {
throw new Error((result as GramJs.StatsGraphError).error);
return undefined;
}
const data = JSON.parse((result as GramJs.StatsGraph).json.data);

View File

@ -83,7 +83,9 @@ export {
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
} from './reactions';
export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics';
export {
fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics, fetchStatisticsAsyncGraph,
} from './statistics';
export {
acceptPhoneCall, confirmPhoneCall, requestPhoneCall, decodePhoneCallData, createPhoneCallState,

View File

@ -2,12 +2,14 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import {
ApiChat, ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph,
ApiChat, ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsGraph,
} from '../../types';
import { invokeRequest } from './client';
import { buildInputEntity } from '../gramjsBuilders';
import { buildChannelStatistics, buildGroupStatistics, buildGraph } from '../apiBuilders/statistics';
import {
buildChannelStatistics, buildGroupStatistics, buildMessageStatistics, buildGraph,
} from '../apiBuilders/statistics';
export async function fetchChannelStatistics({
chat,
@ -37,6 +39,25 @@ export async function fetchGroupStatistics({
return buildGroupStatistics(result);
}
export async function fetchMessageStatistics({
chat,
messageId,
}: {
chat: ApiChat;
messageId: number;
}): Promise<ApiMessageStatistics | undefined> {
const result = await invokeRequest(new GramJs.stats.GetMessageStats({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
msgId: messageId,
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId);
if (!result) {
return undefined;
}
return buildMessageStatistics(result);
}
export async function fetchStatisticsAsyncGraph({
token,
x,

View File

@ -297,6 +297,7 @@ export interface ApiMessage {
isDeleting?: boolean;
previousLocalId?: number;
views?: number;
forwards?: number;
isEdited?: boolean;
editDate?: number;
isMentioned?: boolean;

View File

@ -1,10 +1,10 @@
import { ApiMessage } from './messages';
export interface ApiChannelStatistics {
growthGraph: StatisticsGraph;
followersGraph: StatisticsGraph;
muteGraph: StatisticsGraph;
topHoursGraph: StatisticsGraph;
growthGraph?: StatisticsGraph | string;
followersGraph?: StatisticsGraph | string;
muteGraph?: StatisticsGraph | string;
topHoursGraph?: StatisticsGraph | string;
interactionsGraph: StatisticsGraph | string;
viewsBySourceGraph: StatisticsGraph | string;
newFollowersBySourceGraph: StatisticsGraph | string;
@ -17,9 +17,9 @@ export interface ApiChannelStatistics {
}
export interface ApiGroupStatistics {
growthGraph: StatisticsGraph;
membersGraph: StatisticsGraph;
topHoursGraph: StatisticsGraph;
growthGraph?: StatisticsGraph | string;
membersGraph?: StatisticsGraph | string;
topHoursGraph?: StatisticsGraph | string;
languagesGraph: StatisticsGraph | string;
messagesGraph: StatisticsGraph | string;
actionsGraph: StatisticsGraph | string;
@ -30,6 +30,12 @@ export interface ApiGroupStatistics {
posters: StatisticsOverviewItem;
}
export interface ApiMessageStatistics {
viewsGraph?: StatisticsGraph | string;
forwards?: number;
views?: number;
}
export interface StatisticsGraph {
type: string;
zoomToken?: string;

View File

@ -58,6 +58,7 @@ export { default as StickerSearch } from '../components/right/StickerSearch';
// eslint-disable-next-line import/no-cycle
export { default as GifSearch } from '../components/right/GifSearch';
export { default as Statistics } from '../components/right/statistics/Statistics';
export { default as MessageStatistics } from '../components/right/statistics/MessageStatistics';
export { default as PollResults } from '../components/right/PollResults';
export { default as Management } from '../components/right/management/Management';

View File

@ -25,6 +25,7 @@ import Transition from '../ui/Transition';
import RightSearch from './RightSearch.async';
import Management from './management/Management.async';
import Statistics from './statistics/Statistics.async';
import MessageStatistics from './statistics/MessageStatistics.async';
import StickerSearch from './StickerSearch.async';
import GifSearch from './GifSearch.async';
import PollResults from './PollResults.async';
@ -71,6 +72,7 @@ const RightColumn: FC<StateProps> = ({
setNewChatMembersDialogState,
setEditingExportedInvite,
toggleStatistics,
toggleMessageStatistics,
setOpenedInviteInfo,
requestNextManagementScreen,
} = getActions();
@ -87,6 +89,7 @@ const RightColumn: FC<StateProps> = ({
const isSearch = contentKey === RightColumnContent.Search;
const isManagement = contentKey === RightColumnContent.Management;
const isStatistics = contentKey === RightColumnContent.Statistics;
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
const isGifSearch = contentKey === RightColumnContent.GifSearch;
const isPollResults = contentKey === RightColumnContent.PollResults;
@ -150,6 +153,9 @@ const RightColumn: FC<StateProps> = ({
break;
}
case RightColumnContent.MessageStatistics:
toggleMessageStatistics();
break;
case RightColumnContent.Statistics:
toggleStatistics();
break;
@ -174,7 +180,7 @@ const RightColumn: FC<StateProps> = ({
}, [
contentKey, isScrolledDown, toggleChatInfo, closePollResults, setNewChatMembersDialogState,
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
setEditingExportedInvite, chatId, setOpenedInviteInfo, toggleStatistics,
setEditingExportedInvite, chatId, setOpenedInviteInfo, toggleStatistics, toggleMessageStatistics,
]);
const handleSelectChatMember = useCallback((memberId, isPromoted) => {
@ -268,6 +274,8 @@ const RightColumn: FC<StateProps> = ({
case RightColumnContent.Statistics:
return <Statistics chatId={chatId!} isActive={isOpen && isActive} />;
case RightColumnContent.MessageStatistics:
return <MessageStatistics chatId={chatId!} isActive={isOpen && isActive} />;
case RightColumnContent.StickerSearch:
return <StickerSearch onClose={close} isActive={isOpen && isActive} />;
case RightColumnContent.GifSearch:
@ -293,6 +301,7 @@ const RightColumn: FC<StateProps> = ({
isSearch={isSearch}
isManagement={isManagement}
isStatistics={isStatistics}
isMessageStatistics={isMessageStatistics}
isStickerSearch={isStickerSearch}
isGifSearch={isGifSearch}
isPollResults={isPollResults}

View File

@ -40,6 +40,7 @@ type OwnProps = {
isSearch?: boolean;
isManagement?: boolean;
isStatistics?: boolean;
isMessageStatistics?: boolean;
isStickerSearch?: boolean;
isGifSearch?: boolean;
isPollResults?: boolean;
@ -73,6 +74,7 @@ enum HeaderContent {
SharedMedia,
Search,
Statistics,
MessageStatistics,
Management,
ManageInitial,
ManageChannelSubscribers,
@ -107,6 +109,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
isSearch,
isManagement,
isStatistics,
isMessageStatistics,
isStickerSearch,
isGifSearch,
isPollResults,
@ -246,6 +249,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
) : undefined // Never reached
) : isStatistics ? (
HeaderContent.Statistics
) : isMessageStatistics ? (
HeaderContent.MessageStatistics
) : undefined; // When column is closed
const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1;
@ -373,6 +378,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
);
case HeaderContent.Statistics:
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
case HeaderContent.MessageStatistics:
return <h3>{lang('Stats.MessageTitle')}</h3>;
case HeaderContent.SharedMedia:
return <h3>{lang('SharedMedia')}</h3>;
case HeaderContent.ManageChannelSubscribers:

View File

@ -0,0 +1,16 @@
import React, { FC } from '../../../lib/teact/teact';
import { Bundles } from '../../../util/moduleLoader';
import { OwnProps } from './MessageStatistics';
import useModuleLoader from '../../../hooks/useModuleLoader';
import Loading from '../../ui/Loading';
const MessageStatisticsAsync: FC<OwnProps> = (props) => {
const MessageStatistics = useModuleLoader(Bundles.Extra, 'MessageStatistics');
// eslint-disable-next-line react/jsx-props-no-spreading
return MessageStatistics ? <MessageStatistics {...props} /> : <Loading />;
};
export default MessageStatisticsAsync;

View File

@ -0,0 +1,170 @@
import React, {
FC, memo, useState, useEffect, useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import { callApi } from '../../../api/gramjs';
import { ApiMessageStatistics, StatisticsGraph } from '../../../api/types';
import { selectChat } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import useForceUpdate from '../../../hooks/useForceUpdate';
import Loading from '../../ui/Loading';
import StatisticsOverview from './StatisticsOverview';
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 GRAPH_TITLES = {
viewsGraph: 'Stats.MessageInteractionsTitle',
};
const GRAPHS = Object.keys(GRAPH_TITLES) as (keyof ApiMessageStatistics)[];
export type OwnProps = {
chatId: string;
isActive: boolean;
};
export type StateProps = {
statistics: ApiMessageStatistics;
messageId?: number;
dcId?: number;
};
const Statistics: FC<OwnProps & StateProps> = ({
chatId,
isActive,
statistics,
dcId,
messageId,
}) => {
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 { loadMessageStatistics, loadStatisticsAsyncGraph } = getActions();
const forceUpdate = useForceUpdate();
useEffect(() => {
if (messageId) {
loadMessageStatistics({ chatId, messageId });
}
}, [chatId, loadMessageStatistics, messageId]);
useEffect(() => {
if (!isActive || messageId) {
loadedCharts.current = [];
setIsReady(false);
}
}, [isActive, messageId]);
// 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 });
}
});
}, [chatId, statistics, loadStatisticsAsyncGraph]);
useEffect(() => {
(async () => {
await ensureLovelyChart();
if (!isReady) {
setIsReady(true);
return;
}
if (!statistics) {
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 as StatisticsGraph;
LovelyChart.create(
containerRef.current!.children[index],
{
title: lang((GRAPH_TITLES 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);
});
forceUpdate();
})();
}, [
isReady, statistics, lang, chatId, messageId, loadStatisticsAsyncGraph, dcId, forceUpdate,
]);
if (!isReady || !statistics || !messageId) {
return <Loading />;
}
return (
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
<StatisticsOverview statistics={statistics} isMessage />
{!loadedCharts.current.length && <Loading />}
<div ref={containerRef}>
{GRAPHS.map((graph) => (
<div className={buildClassName('Statistics__graph', !loadedCharts.current.includes(graph) && 'hidden')} />
))}
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const { currentMessage, currentMessageId } = global.statistics;
const chat = selectChat(global, chatId);
const dcId = chat?.fullInfo?.statisticsDcId;
return { statistics: currentMessage, dcId, messageId: currentMessageId };
},
)(Statistics));

View File

@ -9,6 +9,7 @@ import {
ApiChannelStatistics,
ApiGroupStatistics,
StatisticsRecentMessage as StatisticsRecentMessageType,
StatisticsGraph,
} from '../../../api/types';
import { selectChat, selectStatistics } from '../../../global/selectors';
@ -65,6 +66,7 @@ export type StateProps = {
statistics: ApiChannelStatistics | ApiGroupStatistics;
dcId?: number;
isGroup: boolean;
messageId?: number;
};
const Statistics: FC<OwnProps & StateProps> = ({
@ -73,6 +75,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
statistics,
dcId,
isGroup,
messageId,
}) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
@ -89,6 +92,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
useEffect(() => {
if (!isActive) {
loadedCharts.current = [];
setIsReady(false);
}
}, [isActive]);
@ -131,7 +135,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
return;
}
if (!statistics) {
if (!statistics || !containerRef.current) {
return;
}
@ -143,26 +147,34 @@ const Statistics: FC<OwnProps & StateProps> = ({
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 && {
...zoomToken ? {
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
zoomOutLabel: lang('Graph.ZoomOut'),
},
...graph,
} : {},
...graph as StatisticsGraph,
},
);
loadedCharts.current.push(name);
});
})();
}, [graphs, graphTitles, isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId]);
}, [
graphs, graphTitles, isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId,
]);
if (!isReady || !statistics) {
if (!isReady || !statistics || messageId) {
return <Loading />;
}
@ -197,7 +209,11 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId);
const dcId = chat?.fullInfo?.statisticsDcId;
const isGroup = chat?.type === 'chatTypeSuperGroup';
// Show Loading component if message was already selected for improving transition animation
const messageId = global.statistics.currentMessageId;
return { statistics, dcId, isGroup };
return {
statistics, dcId, isGroup, messageId,
};
},
)(Statistics));

View File

@ -32,6 +32,10 @@
&__table {
width: 100%;
&-cell {
width: 50%;
}
&-heading {
font-size: 0.9375rem;
color: var(--color-text-secondary);

View File

@ -1,6 +1,8 @@
import React, { FC, memo } from '../../../lib/teact/teact';
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsOverviewItem } from '../../../api/types';
import {
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsOverviewItem,
} from '../../../api/types';
import { formatIntegerCompact } from '../../../util/textFormat';
import { formatFullDate } from '../../../util/dateFormat';
@ -13,6 +15,8 @@ type OverviewCell = {
name: string;
title: string;
isPercentage?: boolean;
isPlain?: boolean;
isApproximate?: boolean;
};
const CHANNEL_OVERVIEW: OverviewCell[][] = [
@ -37,12 +41,22 @@ const GROUP_OVERVIEW: OverviewCell[][] = [
],
];
const MESSAGE_OVERVIEW: OverviewCell[][] = [
[
{ name: 'views', title: 'StatisticViews', isPlain: true },
{
name: 'forwards', title: 'PrivateShares', isPlain: true, isApproximate: true,
},
],
];
export type OwnProps = {
isGroup?: boolean;
statistics: ApiChannelStatistics | ApiGroupStatistics;
isMessage?: boolean;
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics;
};
const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
const StatisticsOverview: FC<OwnProps> = ({ isGroup, isMessage, statistics }) => {
const lang = useLang();
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
@ -70,7 +84,7 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
return (
<div className="StatisticsOverview">
<div className="StatisticsOverview__header">
<div className="StatisticsOverview__title">{lang('ChannelStats.Overview')}</div>
<div className="StatisticsOverview__title">{lang('StatisticOverview')}</div>
{period && (
<div className="StatisticsOverview__caption">
@ -80,14 +94,23 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
</div>
<table className="StatisticsOverview__table">
{(isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
{(isMessage ? MESSAGE_OVERVIEW : isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
<tr>
{row.map((cell: OverviewCell) => {
const field = (statistics as any)[cell.name];
if (cell.isPlain) {
return (
<td className="StatisticsOverview__table-cell">
<b className="StatisticsOverview__table-value">{cell.isApproximate ? `${field}` : field}</b>
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
</td>
);
}
if (cell.isPercentage) {
return (
<td>
<td className="StatisticsOverview__table-cell">
<b className="StatisticsOverview__table-value">{field.percentage}%</b>
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
</td>
@ -95,7 +118,7 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
}
return (
<td>
<td className="StatisticsOverview__table-cell">
<b className="StatisticsOverview__table-value">
{formatIntegerCompact(field.current)}
</b>

View File

@ -1,5 +1,6 @@
.StatisticsRecentMessage {
position: relative;
cursor: pointer;
margin-bottom: 1rem;
&--with-image {

View File

@ -1,6 +1,7 @@
import React, { FC, memo } from '../../../lib/teact/teact';
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
import useLang, { LangFn } from '../../../hooks/useLang';
import { getActions } from '../../../global';
import { ApiMessage, StatisticsRecentMessage as StatisticsRecentMessageType } from '../../../api/types';
@ -23,17 +24,23 @@ export type OwnProps = {
const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
const lang = useLang();
const { toggleMessageStatistics } = getActions();
const mediaThumbnail = getMessageMediaThumbDataUri(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro'));
const isRoundVideo = Boolean(getMessageRoundVideo(message));
const handleClick = useCallback(() => {
toggleMessageStatistics({ messageId: message.id });
}, [toggleMessageStatistics, message.id]);
return (
<div
className={buildClassName(
'StatisticsRecentMessage',
Boolean(mediaBlobUrl || mediaThumbnail) && 'StatisticsRecentMessage--with-image',
)}
onClick={handleClick}
>
<div className="StatisticsRecentMessage__title">
<div className="StatisticsRecentMessage__summary">

View File

@ -2,7 +2,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { ApiChannelStatistics } from '../../../api/types';
import { callApi } from '../../../api/gramjs';
import { updateStatistics, updateStatisticsGraph } from '../../reducers';
import { updateStatistics, updateMessageStatistics, updateStatisticsGraph } from '../../reducers';
import { selectChatMessages, selectChat } from '../../selectors';
addActionHandler('loadStatistics', async (global, actions, payload) => {
@ -29,6 +29,28 @@ addActionHandler('loadStatistics', async (global, actions, payload) => {
setGlobal(updateStatistics(global, chatId, result));
});
addActionHandler('loadMessageStatistics', async (global, actions, payload) => {
const { chatId, messageId } = payload;
const chat = selectChat(global, chatId);
if (!chat?.fullInfo) {
return;
}
let result = await callApi('fetchMessageStatistics', { chat, messageId });
if (!result) {
result = {};
}
global = getGlobal();
const { views, forwards } = selectChatMessages(global, chatId)[messageId];
result.views = views;
result.forwards = forwards;
setGlobal(updateMessageStatistics(global, result));
});
addActionHandler('loadStatisticsAsyncGraph', async (global, actions, payload) => {
const {
chatId, token, name, isPercentage,

View File

@ -114,6 +114,20 @@ addActionHandler('toggleStatistics', (global) => {
return {
...global,
isStatisticsShown: !global.isStatisticsShown,
statistics: {
...global.statistics,
currentMessageId: undefined,
},
};
});
addActionHandler('toggleMessageStatistics', (global, action, payload) => {
return {
...global,
statistics: {
...global.statistics,
currentMessageId: payload?.messageId,
},
};
});

View File

@ -201,6 +201,7 @@ export const INITIAL_STATE: GlobalState = {
statistics: {
byChatId: {},
currentMessage: {},
},
pollModal: {

View File

@ -1,5 +1,7 @@
import { GlobalState } from '../types';
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph } from '../../api/types';
import {
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsGraph,
} from '../../api/types';
export function updateStatistics(
global: GlobalState, chatId: string, statistics: ApiChannelStatistics | ApiGroupStatistics,
@ -7,6 +9,7 @@ export function updateStatistics(
return {
...global,
statistics: {
currentMessage: {},
byChatId: {
...global.statistics.byChatId,
[chatId]: statistics,
@ -15,12 +18,25 @@ export function updateStatistics(
};
}
export function updateMessageStatistics(
global: GlobalState, statistics: ApiMessageStatistics,
): GlobalState {
return {
...global,
statistics: {
...global.statistics,
currentMessage: statistics,
},
};
}
export function updateStatisticsGraph(
global: GlobalState, chatId: string, name: string, update: StatisticsGraph,
): GlobalState {
return {
...global,
statistics: {
...global.statistics,
byChatId: {
...global.statistics.byChatId,
[chatId]: {

View File

@ -17,3 +17,11 @@ export function selectIsStatisticsShown(global: GlobalState) {
return chat?.fullInfo?.canViewStatistics;
}
export function selectIsMessageStatisticsShown(global: GlobalState) {
if (!global.isStatisticsShown) {
return false;
}
return Boolean(global.statistics.currentMessageId);
}

View File

@ -5,7 +5,7 @@ import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'
import { selectCurrentMessageList, selectIsPollResultsOpen } from './messages';
import { selectCurrentTextSearch } from './localSearch';
import { selectCurrentStickerSearch, selectCurrentGifSearch } from './symbols';
import { selectIsStatisticsShown } from './statistics';
import { selectIsStatisticsShown, selectIsMessageStatisticsShown } from './statistics';
import { selectCurrentManagement } from './management';
export function selectIsMediaViewerOpen(global: GlobalState) {
@ -20,6 +20,8 @@ export function selectRightColumnContentKey(global: GlobalState) {
RightColumnContent.Search
) : selectCurrentManagement(global) ? (
RightColumnContent.Management
) : selectIsMessageStatisticsShown(global) ? (
RightColumnContent.MessageStatistics
) : selectIsStatisticsShown(global) ? (
RightColumnContent.Statistics
) : selectCurrentStickerSearch(global).query !== undefined ? (

View File

@ -28,6 +28,7 @@ import {
ApiSponsoredMessage,
ApiChannelStatistics,
ApiGroupStatistics,
ApiMessageStatistics,
ApiPaymentFormNativeParams,
ApiUpdate,
ApiReportReason,
@ -518,6 +519,8 @@ export type GlobalState = {
statistics: {
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
currentMessage: ApiMessageStatistics;
currentMessageId?: number;
};
newContact?: {
@ -809,7 +812,7 @@ export type NonTypedActionNames = (
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
'openSeenByModal' | 'closeSeenByModal' | 'closeReactorListModal' | 'openReactorListModal' |
'toggleStatistics' |
'toggleStatistics' | 'toggleMessageStatistics' |
// auth
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' |
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
@ -896,7 +899,7 @@ export type NonTypedActionNames = (
'createGroupCall' | 'joinVoiceChatByLink' | 'subscribeToGroupCallUpdates' | 'createGroupCallInviteLink' |
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' |
// stats
'loadStatistics' | 'loadStatisticsAsyncGraph'
'loadStatistics' | 'loadMessageStatistics' | 'loadStatisticsAsyncGraph'
);
const typed = typify<GlobalState, ActionPayloads, NonTypedActionNames>();

View File

@ -1217,4 +1217,5 @@ langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;`;
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;`;

View File

@ -222,6 +222,7 @@
"help.getAppConfig",
"stats.getBroadcastStats",
"stats.getMegagroupStats",
"stats.getMessageStats",
"stats.loadAsyncGraph",
"messages.getAttachMenuBots",
"messages.getAttachMenuBot",

View File

@ -292,6 +292,7 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
return statsFormatDayHourFull(data.xLabels[labelIndex].value);
case 'statsTooltipFormat(\'day\')':
return getLabelDate(data.xLabels[labelIndex]);
case 'statsTooltipFormat(\'hour\')':
case 'statsTooltipFormat(\'5min\')':
return getLabelTime(data.xLabels[labelIndex]);
default:

View File

@ -26,13 +26,7 @@ export function createZoomer(data, overviewData, colors, stateManager, container
const { value } = label;
const dataPromise = data.shouldZoomToPie ? Promise.resolve(_generatePieData(labelIndex)) : data.onZoom(value);
dataPromise
.then((newData) => _replaceData(newData, labelIndex, label))
.catch(() => {
tooltip.toggleLoading(false);
tooltip.toggleIsZoomed(false);
header.toggleIsZooming(false);
});
dataPromise.then((newData) => _replaceData(newData, labelIndex, label));
}
function zoomOut(state) {
@ -58,6 +52,14 @@ export function createZoomer(data, overviewData, colors, stateManager, container
}
function _replaceData(newRawData, labelIndex, zoomInLabel) {
if (!newRawData) {
tooltip.toggleLoading(false);
tooltip.toggleIsZoomed(false);
header.toggleIsZooming(false);
return;
}
tooltip.toggleLoading(false);
const labelWidth = 1 / data.xLabels.length;

View File

@ -1,5 +1,5 @@
import { getMaxMin } from './utils';
import { statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format';
import { statsFormatHour, statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format';
export function analyzeData(data) {
const { title, labelFormatter, tooltipFormatter, isStacked, isPercentage, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel } = data;
@ -28,6 +28,7 @@ export function analyzeData(data) {
case 'statsFormat(\'day\')':
xLabels = statsFormatDay(labels);
break;
case 'statsFormat(\'hour\')':
case 'statsFormat(\'5min\')':
xLabels = statsFormatMin(labels);
break;

View File

@ -239,6 +239,7 @@ export enum RightColumnContent {
Search,
Management,
Statistics,
MessageStatistics,
StickerSearch,
GifSearch,
PollResults,