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>, (
|
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
|
||||||
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
|
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
|
||||||
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
|
'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,
|
date: mtpMessage.date,
|
||||||
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
|
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
|
||||||
views: mtpMessage.views,
|
views: mtpMessage.views,
|
||||||
|
forwards: mtpMessage.forwards,
|
||||||
isFromScheduled: mtpMessage.fromScheduled,
|
isFromScheduled: mtpMessage.fromScheduled,
|
||||||
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
|
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
|
||||||
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
|||||||
import {
|
import {
|
||||||
ApiChannelStatistics,
|
ApiChannelStatistics,
|
||||||
ApiGroupStatistics,
|
ApiGroupStatistics,
|
||||||
|
ApiMessageStatistics,
|
||||||
StatisticsGraph,
|
StatisticsGraph,
|
||||||
StatisticsOverviewItem,
|
StatisticsOverviewItem,
|
||||||
StatisticsOverviewPercentage,
|
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) {
|
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);
|
const data = JSON.parse((result as GramJs.StatsGraph).json.data);
|
||||||
|
|||||||
@ -83,7 +83,9 @@ export {
|
|||||||
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
|
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
|
||||||
} from './reactions';
|
} from './reactions';
|
||||||
|
|
||||||
export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics';
|
export {
|
||||||
|
fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics, fetchStatisticsAsyncGraph,
|
||||||
|
} from './statistics';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
acceptPhoneCall, confirmPhoneCall, requestPhoneCall, decodePhoneCallData, createPhoneCallState,
|
acceptPhoneCall, confirmPhoneCall, requestPhoneCall, decodePhoneCallData, createPhoneCallState,
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import BigInt from 'big-integer';
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiChat, ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph,
|
ApiChat, ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsGraph,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import { buildInputEntity } from '../gramjsBuilders';
|
import { buildInputEntity } from '../gramjsBuilders';
|
||||||
import { buildChannelStatistics, buildGroupStatistics, buildGraph } from '../apiBuilders/statistics';
|
import {
|
||||||
|
buildChannelStatistics, buildGroupStatistics, buildMessageStatistics, buildGraph,
|
||||||
|
} from '../apiBuilders/statistics';
|
||||||
|
|
||||||
export async function fetchChannelStatistics({
|
export async function fetchChannelStatistics({
|
||||||
chat,
|
chat,
|
||||||
@ -37,6 +39,25 @@ export async function fetchGroupStatistics({
|
|||||||
return buildGroupStatistics(result);
|
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({
|
export async function fetchStatisticsAsyncGraph({
|
||||||
token,
|
token,
|
||||||
x,
|
x,
|
||||||
|
|||||||
@ -297,6 +297,7 @@ export interface ApiMessage {
|
|||||||
isDeleting?: boolean;
|
isDeleting?: boolean;
|
||||||
previousLocalId?: number;
|
previousLocalId?: number;
|
||||||
views?: number;
|
views?: number;
|
||||||
|
forwards?: number;
|
||||||
isEdited?: boolean;
|
isEdited?: boolean;
|
||||||
editDate?: number;
|
editDate?: number;
|
||||||
isMentioned?: boolean;
|
isMentioned?: boolean;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { ApiMessage } from './messages';
|
import { ApiMessage } from './messages';
|
||||||
|
|
||||||
export interface ApiChannelStatistics {
|
export interface ApiChannelStatistics {
|
||||||
growthGraph: StatisticsGraph;
|
growthGraph?: StatisticsGraph | string;
|
||||||
followersGraph: StatisticsGraph;
|
followersGraph?: StatisticsGraph | string;
|
||||||
muteGraph: StatisticsGraph;
|
muteGraph?: StatisticsGraph | string;
|
||||||
topHoursGraph: StatisticsGraph;
|
topHoursGraph?: StatisticsGraph | string;
|
||||||
interactionsGraph: StatisticsGraph | string;
|
interactionsGraph: StatisticsGraph | string;
|
||||||
viewsBySourceGraph: StatisticsGraph | string;
|
viewsBySourceGraph: StatisticsGraph | string;
|
||||||
newFollowersBySourceGraph: StatisticsGraph | string;
|
newFollowersBySourceGraph: StatisticsGraph | string;
|
||||||
@ -17,9 +17,9 @@ export interface ApiChannelStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiGroupStatistics {
|
export interface ApiGroupStatistics {
|
||||||
growthGraph: StatisticsGraph;
|
growthGraph?: StatisticsGraph | string;
|
||||||
membersGraph: StatisticsGraph;
|
membersGraph?: StatisticsGraph | string;
|
||||||
topHoursGraph: StatisticsGraph;
|
topHoursGraph?: StatisticsGraph | string;
|
||||||
languagesGraph: StatisticsGraph | string;
|
languagesGraph: StatisticsGraph | string;
|
||||||
messagesGraph: StatisticsGraph | string;
|
messagesGraph: StatisticsGraph | string;
|
||||||
actionsGraph: StatisticsGraph | string;
|
actionsGraph: StatisticsGraph | string;
|
||||||
@ -30,6 +30,12 @@ export interface ApiGroupStatistics {
|
|||||||
posters: StatisticsOverviewItem;
|
posters: StatisticsOverviewItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiMessageStatistics {
|
||||||
|
viewsGraph?: StatisticsGraph | string;
|
||||||
|
forwards?: number;
|
||||||
|
views?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StatisticsGraph {
|
export interface StatisticsGraph {
|
||||||
type: string;
|
type: string;
|
||||||
zoomToken?: string;
|
zoomToken?: string;
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export { default as StickerSearch } from '../components/right/StickerSearch';
|
|||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as GifSearch } from '../components/right/GifSearch';
|
export { default as GifSearch } from '../components/right/GifSearch';
|
||||||
export { default as Statistics } from '../components/right/statistics/Statistics';
|
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 PollResults } from '../components/right/PollResults';
|
||||||
|
|
||||||
export { default as Management } from '../components/right/management/Management';
|
export { default as Management } from '../components/right/management/Management';
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import Transition from '../ui/Transition';
|
|||||||
import RightSearch from './RightSearch.async';
|
import RightSearch from './RightSearch.async';
|
||||||
import Management from './management/Management.async';
|
import Management from './management/Management.async';
|
||||||
import Statistics from './statistics/Statistics.async';
|
import Statistics from './statistics/Statistics.async';
|
||||||
|
import MessageStatistics from './statistics/MessageStatistics.async';
|
||||||
import StickerSearch from './StickerSearch.async';
|
import StickerSearch from './StickerSearch.async';
|
||||||
import GifSearch from './GifSearch.async';
|
import GifSearch from './GifSearch.async';
|
||||||
import PollResults from './PollResults.async';
|
import PollResults from './PollResults.async';
|
||||||
@ -71,6 +72,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
setNewChatMembersDialogState,
|
setNewChatMembersDialogState,
|
||||||
setEditingExportedInvite,
|
setEditingExportedInvite,
|
||||||
toggleStatistics,
|
toggleStatistics,
|
||||||
|
toggleMessageStatistics,
|
||||||
setOpenedInviteInfo,
|
setOpenedInviteInfo,
|
||||||
requestNextManagementScreen,
|
requestNextManagementScreen,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
@ -87,6 +89,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
const isSearch = contentKey === RightColumnContent.Search;
|
const isSearch = contentKey === RightColumnContent.Search;
|
||||||
const isManagement = contentKey === RightColumnContent.Management;
|
const isManagement = contentKey === RightColumnContent.Management;
|
||||||
const isStatistics = contentKey === RightColumnContent.Statistics;
|
const isStatistics = contentKey === RightColumnContent.Statistics;
|
||||||
|
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
|
||||||
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
||||||
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
||||||
const isPollResults = contentKey === RightColumnContent.PollResults;
|
const isPollResults = contentKey === RightColumnContent.PollResults;
|
||||||
@ -150,6 +153,9 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case RightColumnContent.MessageStatistics:
|
||||||
|
toggleMessageStatistics();
|
||||||
|
break;
|
||||||
case RightColumnContent.Statistics:
|
case RightColumnContent.Statistics:
|
||||||
toggleStatistics();
|
toggleStatistics();
|
||||||
break;
|
break;
|
||||||
@ -174,7 +180,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
}, [
|
}, [
|
||||||
contentKey, isScrolledDown, toggleChatInfo, closePollResults, setNewChatMembersDialogState,
|
contentKey, isScrolledDown, toggleChatInfo, closePollResults, setNewChatMembersDialogState,
|
||||||
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
||||||
setEditingExportedInvite, chatId, setOpenedInviteInfo, toggleStatistics,
|
setEditingExportedInvite, chatId, setOpenedInviteInfo, toggleStatistics, toggleMessageStatistics,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleSelectChatMember = useCallback((memberId, isPromoted) => {
|
const handleSelectChatMember = useCallback((memberId, isPromoted) => {
|
||||||
@ -268,6 +274,8 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
|
|
||||||
case RightColumnContent.Statistics:
|
case RightColumnContent.Statistics:
|
||||||
return <Statistics chatId={chatId!} isActive={isOpen && isActive} />;
|
return <Statistics chatId={chatId!} isActive={isOpen && isActive} />;
|
||||||
|
case RightColumnContent.MessageStatistics:
|
||||||
|
return <MessageStatistics chatId={chatId!} isActive={isOpen && isActive} />;
|
||||||
case RightColumnContent.StickerSearch:
|
case RightColumnContent.StickerSearch:
|
||||||
return <StickerSearch onClose={close} isActive={isOpen && isActive} />;
|
return <StickerSearch onClose={close} isActive={isOpen && isActive} />;
|
||||||
case RightColumnContent.GifSearch:
|
case RightColumnContent.GifSearch:
|
||||||
@ -293,6 +301,7 @@ const RightColumn: FC<StateProps> = ({
|
|||||||
isSearch={isSearch}
|
isSearch={isSearch}
|
||||||
isManagement={isManagement}
|
isManagement={isManagement}
|
||||||
isStatistics={isStatistics}
|
isStatistics={isStatistics}
|
||||||
|
isMessageStatistics={isMessageStatistics}
|
||||||
isStickerSearch={isStickerSearch}
|
isStickerSearch={isStickerSearch}
|
||||||
isGifSearch={isGifSearch}
|
isGifSearch={isGifSearch}
|
||||||
isPollResults={isPollResults}
|
isPollResults={isPollResults}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ type OwnProps = {
|
|||||||
isSearch?: boolean;
|
isSearch?: boolean;
|
||||||
isManagement?: boolean;
|
isManagement?: boolean;
|
||||||
isStatistics?: boolean;
|
isStatistics?: boolean;
|
||||||
|
isMessageStatistics?: boolean;
|
||||||
isStickerSearch?: boolean;
|
isStickerSearch?: boolean;
|
||||||
isGifSearch?: boolean;
|
isGifSearch?: boolean;
|
||||||
isPollResults?: boolean;
|
isPollResults?: boolean;
|
||||||
@ -73,6 +74,7 @@ enum HeaderContent {
|
|||||||
SharedMedia,
|
SharedMedia,
|
||||||
Search,
|
Search,
|
||||||
Statistics,
|
Statistics,
|
||||||
|
MessageStatistics,
|
||||||
Management,
|
Management,
|
||||||
ManageInitial,
|
ManageInitial,
|
||||||
ManageChannelSubscribers,
|
ManageChannelSubscribers,
|
||||||
@ -107,6 +109,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
isSearch,
|
isSearch,
|
||||||
isManagement,
|
isManagement,
|
||||||
isStatistics,
|
isStatistics,
|
||||||
|
isMessageStatistics,
|
||||||
isStickerSearch,
|
isStickerSearch,
|
||||||
isGifSearch,
|
isGifSearch,
|
||||||
isPollResults,
|
isPollResults,
|
||||||
@ -246,6 +249,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
) : undefined // Never reached
|
) : undefined // Never reached
|
||||||
) : isStatistics ? (
|
) : isStatistics ? (
|
||||||
HeaderContent.Statistics
|
HeaderContent.Statistics
|
||||||
|
) : isMessageStatistics ? (
|
||||||
|
HeaderContent.MessageStatistics
|
||||||
) : undefined; // When column is closed
|
) : undefined; // When column is closed
|
||||||
|
|
||||||
const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1;
|
const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1;
|
||||||
@ -373,6 +378,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
case HeaderContent.Statistics:
|
case HeaderContent.Statistics:
|
||||||
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
|
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
|
||||||
|
case HeaderContent.MessageStatistics:
|
||||||
|
return <h3>{lang('Stats.MessageTitle')}</h3>;
|
||||||
case HeaderContent.SharedMedia:
|
case HeaderContent.SharedMedia:
|
||||||
return <h3>{lang('SharedMedia')}</h3>;
|
return <h3>{lang('SharedMedia')}</h3>;
|
||||||
case HeaderContent.ManageChannelSubscribers:
|
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,
|
ApiChannelStatistics,
|
||||||
ApiGroupStatistics,
|
ApiGroupStatistics,
|
||||||
StatisticsRecentMessage as StatisticsRecentMessageType,
|
StatisticsRecentMessage as StatisticsRecentMessageType,
|
||||||
|
StatisticsGraph,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import { selectChat, selectStatistics } from '../../../global/selectors';
|
import { selectChat, selectStatistics } from '../../../global/selectors';
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ export type StateProps = {
|
|||||||
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
||||||
dcId?: number;
|
dcId?: number;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
|
messageId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Statistics: FC<OwnProps & StateProps> = ({
|
const Statistics: FC<OwnProps & StateProps> = ({
|
||||||
@ -73,6 +75,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
statistics,
|
statistics,
|
||||||
dcId,
|
dcId,
|
||||||
isGroup,
|
isGroup,
|
||||||
|
messageId,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -89,6 +92,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
loadedCharts.current = [];
|
loadedCharts.current = [];
|
||||||
|
setIsReady(false);
|
||||||
}
|
}
|
||||||
}, [isActive]);
|
}, [isActive]);
|
||||||
|
|
||||||
@ -131,7 +135,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statistics) {
|
if (!statistics || !containerRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,26 +147,34 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!graph) {
|
||||||
|
loadedCharts.current.push(name);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { zoomToken } = graph;
|
const { zoomToken } = graph;
|
||||||
|
|
||||||
LovelyChart.create(
|
LovelyChart.create(
|
||||||
containerRef.current!.children[index],
|
containerRef.current!.children[index],
|
||||||
{
|
{
|
||||||
title: lang((graphTitles as Record<string, string>)[name]),
|
title: lang((graphTitles as Record<string, string>)[name]),
|
||||||
...zoomToken && {
|
...zoomToken ? {
|
||||||
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
|
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
|
||||||
zoomOutLabel: lang('Graph.ZoomOut'),
|
zoomOutLabel: lang('Graph.ZoomOut'),
|
||||||
},
|
} : {},
|
||||||
...graph,
|
...graph as StatisticsGraph,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
loadedCharts.current.push(name);
|
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 />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +209,11 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const dcId = chat?.fullInfo?.statisticsDcId;
|
const dcId = chat?.fullInfo?.statisticsDcId;
|
||||||
const isGroup = chat?.type === 'chatTypeSuperGroup';
|
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));
|
)(Statistics));
|
||||||
|
|||||||
@ -32,6 +32,10 @@
|
|||||||
&__table {
|
&__table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&-cell {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
&-heading {
|
&-heading {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import React, { FC, memo } from '../../../lib/teact/teact';
|
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 { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
import { formatFullDate } from '../../../util/dateFormat';
|
import { formatFullDate } from '../../../util/dateFormat';
|
||||||
@ -13,6 +15,8 @@ type OverviewCell = {
|
|||||||
name: string;
|
name: string;
|
||||||
title: string;
|
title: string;
|
||||||
isPercentage?: boolean;
|
isPercentage?: boolean;
|
||||||
|
isPlain?: boolean;
|
||||||
|
isApproximate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHANNEL_OVERVIEW: OverviewCell[][] = [
|
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 = {
|
export type OwnProps = {
|
||||||
isGroup?: boolean;
|
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 lang = useLang();
|
||||||
|
|
||||||
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
|
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
|
||||||
@ -70,7 +84,7 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="StatisticsOverview">
|
<div className="StatisticsOverview">
|
||||||
<div className="StatisticsOverview__header">
|
<div className="StatisticsOverview__header">
|
||||||
<div className="StatisticsOverview__title">{lang('ChannelStats.Overview')}</div>
|
<div className="StatisticsOverview__title">{lang('StatisticOverview')}</div>
|
||||||
|
|
||||||
{period && (
|
{period && (
|
||||||
<div className="StatisticsOverview__caption">
|
<div className="StatisticsOverview__caption">
|
||||||
@ -80,14 +94,23 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table className="StatisticsOverview__table">
|
<table className="StatisticsOverview__table">
|
||||||
{(isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
|
{(isMessage ? MESSAGE_OVERVIEW : isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
|
||||||
<tr>
|
<tr>
|
||||||
{row.map((cell: OverviewCell) => {
|
{row.map((cell: OverviewCell) => {
|
||||||
const field = (statistics as any)[cell.name];
|
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) {
|
if (cell.isPercentage) {
|
||||||
return (
|
return (
|
||||||
<td>
|
<td className="StatisticsOverview__table-cell">
|
||||||
<b className="StatisticsOverview__table-value">{field.percentage}%</b>
|
<b className="StatisticsOverview__table-value">{field.percentage}%</b>
|
||||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||||
</td>
|
</td>
|
||||||
@ -95,7 +118,7 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td>
|
<td className="StatisticsOverview__table-cell">
|
||||||
<b className="StatisticsOverview__table-value">
|
<b className="StatisticsOverview__table-value">
|
||||||
{formatIntegerCompact(field.current)}
|
{formatIntegerCompact(field.current)}
|
||||||
</b>
|
</b>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
.StatisticsRecentMessage {
|
.StatisticsRecentMessage {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
&--with-image {
|
&--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 useLang, { LangFn } from '../../../hooks/useLang';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import { ApiMessage, StatisticsRecentMessage as StatisticsRecentMessageType } from '../../../api/types';
|
import { ApiMessage, StatisticsRecentMessage as StatisticsRecentMessageType } from '../../../api/types';
|
||||||
|
|
||||||
@ -23,17 +24,23 @@ export type OwnProps = {
|
|||||||
|
|
||||||
const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
const { toggleMessageStatistics } = getActions();
|
||||||
|
|
||||||
const mediaThumbnail = getMessageMediaThumbDataUri(message);
|
const mediaThumbnail = getMessageMediaThumbDataUri(message);
|
||||||
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro'));
|
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'micro'));
|
||||||
const isRoundVideo = Boolean(getMessageRoundVideo(message));
|
const isRoundVideo = Boolean(getMessageRoundVideo(message));
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
toggleMessageStatistics({ messageId: message.id });
|
||||||
|
}, [toggleMessageStatistics, message.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'StatisticsRecentMessage',
|
'StatisticsRecentMessage',
|
||||||
Boolean(mediaBlobUrl || mediaThumbnail) && 'StatisticsRecentMessage--with-image',
|
Boolean(mediaBlobUrl || mediaThumbnail) && 'StatisticsRecentMessage--with-image',
|
||||||
)}
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div className="StatisticsRecentMessage__title">
|
<div className="StatisticsRecentMessage__title">
|
||||||
<div className="StatisticsRecentMessage__summary">
|
<div className="StatisticsRecentMessage__summary">
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
|||||||
|
|
||||||
import { ApiChannelStatistics } from '../../../api/types';
|
import { ApiChannelStatistics } from '../../../api/types';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { updateStatistics, updateStatisticsGraph } from '../../reducers';
|
import { updateStatistics, updateMessageStatistics, updateStatisticsGraph } from '../../reducers';
|
||||||
import { selectChatMessages, selectChat } from '../../selectors';
|
import { selectChatMessages, selectChat } from '../../selectors';
|
||||||
|
|
||||||
addActionHandler('loadStatistics', async (global, actions, payload) => {
|
addActionHandler('loadStatistics', async (global, actions, payload) => {
|
||||||
@ -29,6 +29,28 @@ addActionHandler('loadStatistics', async (global, actions, payload) => {
|
|||||||
setGlobal(updateStatistics(global, chatId, result));
|
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) => {
|
addActionHandler('loadStatisticsAsyncGraph', async (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
chatId, token, name, isPercentage,
|
chatId, token, name, isPercentage,
|
||||||
|
|||||||
@ -114,6 +114,20 @@ addActionHandler('toggleStatistics', (global) => {
|
|||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
isStatisticsShown: !global.isStatisticsShown,
|
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: {
|
statistics: {
|
||||||
byChatId: {},
|
byChatId: {},
|
||||||
|
currentMessage: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
pollModal: {
|
pollModal: {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { GlobalState } from '../types';
|
import { GlobalState } from '../types';
|
||||||
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph } from '../../api/types';
|
import {
|
||||||
|
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsGraph,
|
||||||
|
} from '../../api/types';
|
||||||
|
|
||||||
export function updateStatistics(
|
export function updateStatistics(
|
||||||
global: GlobalState, chatId: string, statistics: ApiChannelStatistics | ApiGroupStatistics,
|
global: GlobalState, chatId: string, statistics: ApiChannelStatistics | ApiGroupStatistics,
|
||||||
@ -7,6 +9,7 @@ export function updateStatistics(
|
|||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
statistics: {
|
statistics: {
|
||||||
|
currentMessage: {},
|
||||||
byChatId: {
|
byChatId: {
|
||||||
...global.statistics.byChatId,
|
...global.statistics.byChatId,
|
||||||
[chatId]: statistics,
|
[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(
|
export function updateStatisticsGraph(
|
||||||
global: GlobalState, chatId: string, name: string, update: StatisticsGraph,
|
global: GlobalState, chatId: string, name: string, update: StatisticsGraph,
|
||||||
): GlobalState {
|
): GlobalState {
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
statistics: {
|
statistics: {
|
||||||
|
...global.statistics,
|
||||||
byChatId: {
|
byChatId: {
|
||||||
...global.statistics.byChatId,
|
...global.statistics.byChatId,
|
||||||
[chatId]: {
|
[chatId]: {
|
||||||
|
|||||||
@ -17,3 +17,11 @@ export function selectIsStatisticsShown(global: GlobalState) {
|
|||||||
|
|
||||||
return chat?.fullInfo?.canViewStatistics;
|
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 { selectCurrentMessageList, selectIsPollResultsOpen } from './messages';
|
||||||
import { selectCurrentTextSearch } from './localSearch';
|
import { selectCurrentTextSearch } from './localSearch';
|
||||||
import { selectCurrentStickerSearch, selectCurrentGifSearch } from './symbols';
|
import { selectCurrentStickerSearch, selectCurrentGifSearch } from './symbols';
|
||||||
import { selectIsStatisticsShown } from './statistics';
|
import { selectIsStatisticsShown, selectIsMessageStatisticsShown } from './statistics';
|
||||||
import { selectCurrentManagement } from './management';
|
import { selectCurrentManagement } from './management';
|
||||||
|
|
||||||
export function selectIsMediaViewerOpen(global: GlobalState) {
|
export function selectIsMediaViewerOpen(global: GlobalState) {
|
||||||
@ -20,6 +20,8 @@ export function selectRightColumnContentKey(global: GlobalState) {
|
|||||||
RightColumnContent.Search
|
RightColumnContent.Search
|
||||||
) : selectCurrentManagement(global) ? (
|
) : selectCurrentManagement(global) ? (
|
||||||
RightColumnContent.Management
|
RightColumnContent.Management
|
||||||
|
) : selectIsMessageStatisticsShown(global) ? (
|
||||||
|
RightColumnContent.MessageStatistics
|
||||||
) : selectIsStatisticsShown(global) ? (
|
) : selectIsStatisticsShown(global) ? (
|
||||||
RightColumnContent.Statistics
|
RightColumnContent.Statistics
|
||||||
) : selectCurrentStickerSearch(global).query !== undefined ? (
|
) : selectCurrentStickerSearch(global).query !== undefined ? (
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
ApiSponsoredMessage,
|
ApiSponsoredMessage,
|
||||||
ApiChannelStatistics,
|
ApiChannelStatistics,
|
||||||
ApiGroupStatistics,
|
ApiGroupStatistics,
|
||||||
|
ApiMessageStatistics,
|
||||||
ApiPaymentFormNativeParams,
|
ApiPaymentFormNativeParams,
|
||||||
ApiUpdate,
|
ApiUpdate,
|
||||||
ApiReportReason,
|
ApiReportReason,
|
||||||
@ -518,6 +519,8 @@ export type GlobalState = {
|
|||||||
|
|
||||||
statistics: {
|
statistics: {
|
||||||
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
|
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
|
||||||
|
currentMessage: ApiMessageStatistics;
|
||||||
|
currentMessageId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
newContact?: {
|
newContact?: {
|
||||||
@ -809,7 +812,7 @@ export type NonTypedActionNames = (
|
|||||||
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||||
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
||||||
'openSeenByModal' | 'closeSeenByModal' | 'closeReactorListModal' | 'openReactorListModal' |
|
'openSeenByModal' | 'closeSeenByModal' | 'closeReactorListModal' | 'openReactorListModal' |
|
||||||
'toggleStatistics' |
|
'toggleStatistics' | 'toggleMessageStatistics' |
|
||||||
// auth
|
// auth
|
||||||
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' |
|
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' |
|
||||||
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
||||||
@ -896,7 +899,7 @@ export type NonTypedActionNames = (
|
|||||||
'createGroupCall' | 'joinVoiceChatByLink' | 'subscribeToGroupCallUpdates' | 'createGroupCallInviteLink' |
|
'createGroupCall' | 'joinVoiceChatByLink' | 'subscribeToGroupCallUpdates' | 'createGroupCallInviteLink' |
|
||||||
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' |
|
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' |
|
||||||
// stats
|
// stats
|
||||||
'loadStatistics' | 'loadStatisticsAsyncGraph'
|
'loadStatistics' | 'loadMessageStatistics' | 'loadStatisticsAsyncGraph'
|
||||||
);
|
);
|
||||||
|
|
||||||
const typed = typify<GlobalState, ActionPayloads, NonTypedActionNames>();
|
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;
|
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
|
||||||
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
|
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
|
||||||
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
|
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",
|
"help.getAppConfig",
|
||||||
"stats.getBroadcastStats",
|
"stats.getBroadcastStats",
|
||||||
"stats.getMegagroupStats",
|
"stats.getMegagroupStats",
|
||||||
|
"stats.getMessageStats",
|
||||||
"stats.loadAsyncGraph",
|
"stats.loadAsyncGraph",
|
||||||
"messages.getAttachMenuBots",
|
"messages.getAttachMenuBots",
|
||||||
"messages.getAttachMenuBot",
|
"messages.getAttachMenuBot",
|
||||||
|
|||||||
@ -292,6 +292,7 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
|
|||||||
return statsFormatDayHourFull(data.xLabels[labelIndex].value);
|
return statsFormatDayHourFull(data.xLabels[labelIndex].value);
|
||||||
case 'statsTooltipFormat(\'day\')':
|
case 'statsTooltipFormat(\'day\')':
|
||||||
return getLabelDate(data.xLabels[labelIndex]);
|
return getLabelDate(data.xLabels[labelIndex]);
|
||||||
|
case 'statsTooltipFormat(\'hour\')':
|
||||||
case 'statsTooltipFormat(\'5min\')':
|
case 'statsTooltipFormat(\'5min\')':
|
||||||
return getLabelTime(data.xLabels[labelIndex]);
|
return getLabelTime(data.xLabels[labelIndex]);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -26,13 +26,7 @@ export function createZoomer(data, overviewData, colors, stateManager, container
|
|||||||
|
|
||||||
const { value } = label;
|
const { value } = label;
|
||||||
const dataPromise = data.shouldZoomToPie ? Promise.resolve(_generatePieData(labelIndex)) : data.onZoom(value);
|
const dataPromise = data.shouldZoomToPie ? Promise.resolve(_generatePieData(labelIndex)) : data.onZoom(value);
|
||||||
dataPromise
|
dataPromise.then((newData) => _replaceData(newData, labelIndex, label));
|
||||||
.then((newData) => _replaceData(newData, labelIndex, label))
|
|
||||||
.catch(() => {
|
|
||||||
tooltip.toggleLoading(false);
|
|
||||||
tooltip.toggleIsZoomed(false);
|
|
||||||
header.toggleIsZooming(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function zoomOut(state) {
|
function zoomOut(state) {
|
||||||
@ -58,6 +52,14 @@ export function createZoomer(data, overviewData, colors, stateManager, container
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _replaceData(newRawData, labelIndex, zoomInLabel) {
|
function _replaceData(newRawData, labelIndex, zoomInLabel) {
|
||||||
|
if (!newRawData) {
|
||||||
|
tooltip.toggleLoading(false);
|
||||||
|
tooltip.toggleIsZoomed(false);
|
||||||
|
header.toggleIsZooming(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tooltip.toggleLoading(false);
|
tooltip.toggleLoading(false);
|
||||||
|
|
||||||
const labelWidth = 1 / data.xLabels.length;
|
const labelWidth = 1 / data.xLabels.length;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { getMaxMin } from './utils';
|
import { getMaxMin } from './utils';
|
||||||
import { statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format';
|
import { statsFormatHour, statsFormatDay, statsFormatDayHour, statsFormatText, statsFormatMin } from './format';
|
||||||
|
|
||||||
export function analyzeData(data) {
|
export function analyzeData(data) {
|
||||||
const { title, labelFormatter, tooltipFormatter, isStacked, isPercentage, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel } = data;
|
const { title, labelFormatter, tooltipFormatter, isStacked, isPercentage, hasSecondYAxis, onZoom, minimapRange, hideCaption, zoomOutLabel } = data;
|
||||||
@ -28,6 +28,7 @@ export function analyzeData(data) {
|
|||||||
case 'statsFormat(\'day\')':
|
case 'statsFormat(\'day\')':
|
||||||
xLabels = statsFormatDay(labels);
|
xLabels = statsFormatDay(labels);
|
||||||
break;
|
break;
|
||||||
|
case 'statsFormat(\'hour\')':
|
||||||
case 'statsFormat(\'5min\')':
|
case 'statsFormat(\'5min\')':
|
||||||
xLabels = statsFormatMin(labels);
|
xLabels = statsFormatMin(labels);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -239,6 +239,7 @@ export enum RightColumnContent {
|
|||||||
Search,
|
Search,
|
||||||
Management,
|
Management,
|
||||||
Statistics,
|
Statistics,
|
||||||
|
MessageStatistics,
|
||||||
StickerSearch,
|
StickerSearch,
|
||||||
GifSearch,
|
GifSearch,
|
||||||
PollResults,
|
PollResults,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user