Statistics: Posts and messages (#1801)
This commit is contained in:
parent
c3318d93e6
commit
47a26ab9f1
@ -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 }),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -297,6 +297,7 @@ export interface ApiMessage {
|
||||
isDeleting?: boolean;
|
||||
previousLocalId?: number;
|
||||
views?: number;
|
||||
forwards?: number;
|
||||
isEdited?: boolean;
|
||||
editDate?: number;
|
||||
isMentioned?: boolean;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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:
|
||||
|
||||
16
src/components/right/statistics/MessageStatistics.async.tsx
Normal file
16
src/components/right/statistics/MessageStatistics.async.tsx
Normal 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;
|
||||
170
src/components/right/statistics/MessageStatistics.tsx
Normal file
170
src/components/right/statistics/MessageStatistics.tsx
Normal 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));
|
||||
@ -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));
|
||||
|
||||
@ -32,6 +32,10 @@
|
||||
&__table {
|
||||
width: 100%;
|
||||
|
||||
&-cell {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&-heading {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.StatisticsRecentMessage {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&--with-image {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -201,6 +201,7 @@ export const INITIAL_STATE: GlobalState = {
|
||||
|
||||
statistics: {
|
||||
byChatId: {},
|
||||
currentMessage: {},
|
||||
},
|
||||
|
||||
pollModal: {
|
||||
|
||||
@ -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]: {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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;`;
|
||||
@ -222,6 +222,7 @@
|
||||
"help.getAppConfig",
|
||||
"stats.getBroadcastStats",
|
||||
"stats.getMegagroupStats",
|
||||
"stats.getMessageStats",
|
||||
"stats.loadAsyncGraph",
|
||||
"messages.getAttachMenuBots",
|
||||
"messages.getAttachMenuBot",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -239,6 +239,7 @@ export enum RightColumnContent {
|
||||
Search,
|
||||
Management,
|
||||
Statistics,
|
||||
MessageStatistics,
|
||||
StickerSearch,
|
||||
GifSearch,
|
||||
PollResults,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user