mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 13:21:37 +01:00
Management: Group Statistics (#1774)
This commit is contained in:
parent
6d063c2132
commit
05a59608fc
@ -1,12 +1,14 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import {
|
||||
ApiStatistics,
|
||||
ApiChannelStatistics,
|
||||
ApiGroupStatistics,
|
||||
StatisticsGraph,
|
||||
StatisticsOverviewItem,
|
||||
StatisticsOverviewPercentage,
|
||||
StatisticsOverviewPeriod,
|
||||
} from '../../types';
|
||||
|
||||
export function buildStatistics(stats: GramJs.stats.BroadcastStats): ApiStatistics {
|
||||
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics {
|
||||
return {
|
||||
// Graphs
|
||||
growthGraph: buildGraph(stats.growthGraph),
|
||||
@ -31,6 +33,27 @@ export function buildStatistics(stats: GramJs.stats.BroadcastStats): ApiStatisti
|
||||
};
|
||||
}
|
||||
|
||||
export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGroupStatistics {
|
||||
return {
|
||||
// Graphs
|
||||
growthGraph: buildGraph(stats.growthGraph),
|
||||
membersGraph: buildGraph(stats.membersGraph),
|
||||
topHoursGraph: buildGraph(stats.topHoursGraph),
|
||||
|
||||
// Async graphs
|
||||
languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token,
|
||||
messagesGraph: (stats.messagesGraph as GramJs.StatsGraphAsync).token,
|
||||
actionsGraph: (stats.actionsGraph as GramJs.StatsGraphAsync).token,
|
||||
|
||||
// Statistics overview
|
||||
period: getOverviewPeriod(stats.period),
|
||||
members: buildStatisticsOverview(stats.members),
|
||||
viewers: buildStatisticsOverview(stats.viewers),
|
||||
messages: buildStatisticsOverview(stats.messages),
|
||||
posters: buildStatisticsOverview(stats.posters),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean): StatisticsGraph {
|
||||
if ((result as GramJs.StatsGraphError).error) {
|
||||
throw new Error((result as GramJs.StatsGraphError).error);
|
||||
@ -41,7 +64,7 @@ export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean
|
||||
const hasSecondYAxis = data.y_scaled;
|
||||
|
||||
return {
|
||||
type: getGraphType(data.types.y0, isPercentage),
|
||||
type: isPercentage ? 'area' : data.types.y0,
|
||||
zoomToken: (result as GramJs.StatsGraph).zoomToken,
|
||||
labelFormatter: data.xTickFormatter,
|
||||
tooltipFormatter: data.xTooltipFormatter,
|
||||
@ -63,15 +86,6 @@ export function buildGraph(result: GramJs.TypeStatsGraph, isPercentage?: boolean
|
||||
};
|
||||
}
|
||||
|
||||
function getGraphType(apiType: string, isPercentage?: boolean): string {
|
||||
switch (apiType) {
|
||||
case 'step':
|
||||
return 'bar';
|
||||
default:
|
||||
return isPercentage ? 'area' : apiType;
|
||||
}
|
||||
}
|
||||
|
||||
function extractColor(color: string): string {
|
||||
return color.substring(color.indexOf('#'));
|
||||
}
|
||||
@ -113,3 +127,10 @@ function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOv
|
||||
percentage: ((data.part / data.total) * 100).toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewPeriod {
|
||||
return {
|
||||
maxDate: data.maxDate,
|
||||
minDate: data.minDate,
|
||||
};
|
||||
}
|
||||
|
@ -77,4 +77,4 @@ export {
|
||||
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
|
||||
} from './reactions';
|
||||
|
||||
export { fetchStatistics, fetchStatisticsAsyncGraph } from './statistics';
|
||||
export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics';
|
||||
|
@ -1,13 +1,17 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import { ApiChat, ApiStatistics, StatisticsGraph } from '../../types';
|
||||
import {
|
||||
ApiChat, ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph,
|
||||
} from '../../types';
|
||||
|
||||
import { invokeRequest } from './client';
|
||||
import { buildInputEntity } from '../gramjsBuilders';
|
||||
import { buildStatistics, buildGraph } from '../apiBuilders/statistics';
|
||||
import { buildChannelStatistics, buildGroupStatistics, buildGraph } from '../apiBuilders/statistics';
|
||||
|
||||
export async function fetchStatistics({ chat }: { chat: ApiChat }): Promise<ApiStatistics | undefined> {
|
||||
export async function fetchChannelStatistics({
|
||||
chat,
|
||||
}: { chat: ApiChat }): Promise<ApiChannelStatistics | undefined> {
|
||||
const result = await invokeRequest(new GramJs.stats.GetBroadcastStats({
|
||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId);
|
||||
@ -16,7 +20,21 @@ export async function fetchStatistics({ chat }: { chat: ApiChat }): Promise<ApiS
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildStatistics(result);
|
||||
return buildChannelStatistics(result);
|
||||
}
|
||||
|
||||
export async function fetchGroupStatistics({
|
||||
chat,
|
||||
}: { chat: ApiChat }): Promise<ApiGroupStatistics | undefined> {
|
||||
const result = await invokeRequest(new GramJs.stats.GetMegagroupStats({
|
||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||
}), undefined, undefined, undefined, chat.fullInfo!.statisticsDcId);
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildGroupStatistics(result);
|
||||
}
|
||||
|
||||
export async function fetchStatisticsAsyncGraph({
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApiMessage } from './messages';
|
||||
|
||||
export interface ApiStatistics {
|
||||
export interface ApiChannelStatistics {
|
||||
growthGraph: StatisticsGraph;
|
||||
followersGraph: StatisticsGraph;
|
||||
muteGraph: StatisticsGraph;
|
||||
@ -16,6 +16,20 @@ export interface ApiStatistics {
|
||||
recentTopMessages: Array<StatisticsRecentMessage | StatisticsRecentMessage & ApiMessage>;
|
||||
}
|
||||
|
||||
export interface ApiGroupStatistics {
|
||||
growthGraph: StatisticsGraph;
|
||||
membersGraph: StatisticsGraph;
|
||||
topHoursGraph: StatisticsGraph;
|
||||
languagesGraph: StatisticsGraph | string;
|
||||
messagesGraph: StatisticsGraph | string;
|
||||
actionsGraph: StatisticsGraph | string;
|
||||
period: StatisticsOverviewPeriod;
|
||||
members: StatisticsOverviewItem;
|
||||
viewers: StatisticsOverviewItem;
|
||||
messages: StatisticsOverviewItem;
|
||||
posters: StatisticsOverviewItem;
|
||||
}
|
||||
|
||||
export interface StatisticsGraph {
|
||||
type: string;
|
||||
zoomToken?: string;
|
||||
@ -49,6 +63,11 @@ export interface StatisticsOverviewPercentage {
|
||||
percentage: string;
|
||||
}
|
||||
|
||||
export interface StatisticsOverviewPeriod {
|
||||
maxDate: number;
|
||||
minDate: number;
|
||||
}
|
||||
|
||||
export interface StatisticsRecentMessage {
|
||||
msgId: number;
|
||||
forwards: number;
|
||||
|
@ -3,8 +3,9 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
|
||||
&--messages {
|
||||
&__messages {
|
||||
padding: 1rem 0.75rem;
|
||||
border-top: 1px solid var(--color-borders);
|
||||
|
||||
&-title {
|
||||
padding-left: 0.25rem;
|
||||
@ -23,13 +24,18 @@
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
&__graph {
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React, {
|
||||
FC, memo, useState, useEffect, useRef,
|
||||
FC, memo, useState, useEffect, useRef, useMemo,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
ApiMessage, ApiStatistics, StatisticsRecentMessage as StatisticsRecentMessageType, StatisticsGraph,
|
||||
ApiMessage,
|
||||
ApiChannelStatistics,
|
||||
ApiGroupStatistics,
|
||||
StatisticsRecentMessage as StatisticsRecentMessageType,
|
||||
} from '../../../api/types';
|
||||
import { selectChat, selectStatistics } from '../../../global/selectors';
|
||||
|
||||
@ -31,7 +34,7 @@ async function ensureLovelyChart() {
|
||||
return lovelyChartPromise;
|
||||
}
|
||||
|
||||
const GRAPHS_TITLES = {
|
||||
const CHANNEL_GRAPHS_TITLES = {
|
||||
growthGraph: 'ChannelStats.Graph.Growth',
|
||||
followersGraph: 'ChannelStats.Graph.Followers',
|
||||
muteGraph: 'ChannelStats.Graph.Notifications',
|
||||
@ -41,7 +44,17 @@ const GRAPHS_TITLES = {
|
||||
languagesGraph: 'ChannelStats.Graph.Language',
|
||||
interactionsGraph: 'ChannelStats.Graph.Interactions',
|
||||
};
|
||||
const GRAPHS = Object.keys(GRAPHS_TITLES) as (keyof ApiStatistics)[];
|
||||
const CHANNEL_GRAPHS = Object.keys(CHANNEL_GRAPHS_TITLES) as (keyof ApiChannelStatistics)[];
|
||||
|
||||
const GROUP_GRAPHS_TITLES = {
|
||||
growthGraph: 'Stats.GroupGrowthTitle',
|
||||
membersGraph: 'Stats.GroupMembersTitle',
|
||||
languagesGraph: 'Stats.GroupLanguagesTitle',
|
||||
messagesGraph: 'Stats.GroupMessagesTitle',
|
||||
actionsGraph: 'Stats.GroupActionsTitle',
|
||||
topHoursGraph: 'Stats.GroupTopHoursTitle',
|
||||
};
|
||||
const GROUP_GRAPHS = Object.keys(GROUP_GRAPHS_TITLES) as (keyof ApiGroupStatistics)[];
|
||||
|
||||
export type OwnProps = {
|
||||
chatId: string;
|
||||
@ -49,12 +62,17 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
statistics: ApiStatistics;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
||||
dcId?: number;
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
const Statistics: FC<OwnProps & StateProps> = ({
|
||||
chatId, isActive, statistics, dcId,
|
||||
chatId,
|
||||
isActive,
|
||||
statistics,
|
||||
dcId,
|
||||
isGroup,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -65,8 +83,8 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
const { loadStatistics, loadStatisticsAsyncGraph } = getActions();
|
||||
|
||||
useEffect(() => {
|
||||
loadStatistics({ chatId });
|
||||
}, [chatId, loadStatistics]);
|
||||
loadStatistics({ chatId, isGroup });
|
||||
}, [chatId, loadStatistics, isGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
@ -74,25 +92,35 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
const graphs = useMemo(() => {
|
||||
return isGroup ? GROUP_GRAPHS : CHANNEL_GRAPHS;
|
||||
}, [isGroup]);
|
||||
|
||||
const graphTitles = useMemo(() => {
|
||||
return isGroup ? GROUP_GRAPHS_TITLES : CHANNEL_GRAPHS_TITLES;
|
||||
}, [isGroup]);
|
||||
|
||||
// Load async graphs
|
||||
useEffect(() => {
|
||||
if (!statistics) {
|
||||
return;
|
||||
}
|
||||
|
||||
GRAPHS.forEach((graph) => {
|
||||
const isAsync = typeof statistics?.[graph] === 'string';
|
||||
graphs.forEach((name) => {
|
||||
const graph = statistics[name as keyof typeof statistics];
|
||||
const isAsync = typeof graph === 'string';
|
||||
|
||||
if (isAsync) {
|
||||
loadStatisticsAsyncGraph({
|
||||
name: graph,
|
||||
name,
|
||||
chatId,
|
||||
token: statistics[graph],
|
||||
token: graph,
|
||||
// Hardcode percentage for languages graph, since API does not return `percentage` flag
|
||||
isPercentage: graph === 'languagesGraph',
|
||||
isPercentage: name === 'languagesGraph',
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [chatId, statistics, loadStatisticsAsyncGraph]);
|
||||
}, [graphs, chatId, statistics, loadStatisticsAsyncGraph]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -107,30 +135,32 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
GRAPHS.forEach((graph, index: number) => {
|
||||
const isAsync = typeof statistics?.[graph] === 'string';
|
||||
if (isAsync || loadedCharts.current.includes(graph)) {
|
||||
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;
|
||||
}
|
||||
|
||||
const { zoomToken } = (statistics[graph] as StatisticsGraph);
|
||||
const { zoomToken } = graph;
|
||||
|
||||
LovelyChart.create(
|
||||
containerRef.current!.children[index],
|
||||
{
|
||||
title: lang((GRAPHS_TITLES as Record<string, string>)[graph]),
|
||||
title: lang((graphTitles as Record<string, string>)[name]),
|
||||
...zoomToken && {
|
||||
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
|
||||
zoomOutLabel: lang('Graph.ZoomOut'),
|
||||
},
|
||||
...(statistics[graph] as StatisticsGraph),
|
||||
...graph,
|
||||
},
|
||||
);
|
||||
|
||||
loadedCharts.current.push(graph);
|
||||
loadedCharts.current.push(name);
|
||||
});
|
||||
})();
|
||||
}, [isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId]);
|
||||
}, [graphs, graphTitles, isReady, statistics, lang, chatId, loadStatisticsAsyncGraph, dcId]);
|
||||
|
||||
if (!isReady || !statistics) {
|
||||
return <Loading />;
|
||||
@ -138,21 +168,21 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
|
||||
<StatisticsOverview statistics={statistics} />
|
||||
<StatisticsOverview statistics={statistics} isGroup={isGroup} />
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
|
||||
<div ref={containerRef}>
|
||||
{GRAPHS.map((graph) => (
|
||||
<div className={buildClassName('chat-container', !loadedCharts.current.includes(graph) && 'hidden')} />
|
||||
{graphs.map((graph) => (
|
||||
<div className={buildClassName('Statistics__graph', !loadedCharts.current.includes(graph) && 'hidden')} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{Boolean(statistics.recentTopMessages?.length) && (
|
||||
<div className="Statistics--messages">
|
||||
<h2 className="Statistics--messages-title">{lang('ChannelStats.Recent.Header')}</h2>
|
||||
{Boolean((statistics as ApiChannelStatistics).recentTopMessages?.length) && (
|
||||
<div className="Statistics__messages">
|
||||
<h2 className="Statistics__messages-title">{lang('ChannelStats.Recent.Header')}</h2>
|
||||
|
||||
{statistics.recentTopMessages.map((message) => (
|
||||
{(statistics as ApiChannelStatistics).recentTopMessages.map((message) => (
|
||||
<StatisticsRecentMessage message={message as ApiMessage & StatisticsRecentMessageType} />
|
||||
))}
|
||||
</div>
|
||||
@ -164,8 +194,10 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const statistics = selectStatistics(global, chatId);
|
||||
const dcId = selectChat(global, chatId)?.fullInfo?.statisticsDcId;
|
||||
const chat = selectChat(global, chatId);
|
||||
const dcId = chat?.fullInfo?.statisticsDcId;
|
||||
const isGroup = chat?.type === 'chatTypeSuperGroup';
|
||||
|
||||
return { statistics, dcId };
|
||||
return { statistics, dcId, isGroup };
|
||||
},
|
||||
)(Statistics));
|
||||
|
@ -3,7 +3,16 @@
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
|
||||
&--title {
|
||||
&__header {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-right: 2em;
|
||||
padding-left: 0.25rem;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
@ -15,7 +24,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--table {
|
||||
&__caption {
|
||||
font-size: 0.75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__table {
|
||||
width: 100%;
|
||||
|
||||
&-heading {
|
||||
@ -29,7 +43,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--value {
|
||||
&__value {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-green);
|
||||
|
||||
|
@ -1,25 +1,60 @@
|
||||
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||
|
||||
import { ApiStatistics, StatisticsOverviewItem } from '../../../api/types';
|
||||
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsOverviewItem } from '../../../api/types';
|
||||
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { formatFullDate } from '../../../util/dateFormat';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import './StatisticsOverview.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
statistics: ApiStatistics;
|
||||
type OverviewCell = {
|
||||
name: string;
|
||||
title: string;
|
||||
isPercentage?: boolean;
|
||||
};
|
||||
|
||||
const StatisticsOverview: FC<OwnProps> = ({ statistics }) => {
|
||||
const CHANNEL_OVERVIEW: OverviewCell[][] = [
|
||||
[
|
||||
{ name: 'followers', title: 'ChannelStats.Overview.Followers' },
|
||||
{ name: 'enabledNotifications', title: 'ChannelStats.Overview.EnabledNotifications', isPercentage: true },
|
||||
],
|
||||
[
|
||||
{ name: 'viewsPerPost', title: 'ChannelStats.Overview.ViewsPerPost' },
|
||||
{ name: 'sharesPerPost', title: 'ChannelStats.Overview.SharesPerPost' },
|
||||
],
|
||||
];
|
||||
|
||||
const GROUP_OVERVIEW: OverviewCell[][] = [
|
||||
[
|
||||
{ name: 'members', title: 'Stats.GroupMembers' },
|
||||
{ name: 'messages', title: 'Stats.GroupMessages' },
|
||||
],
|
||||
[
|
||||
{ name: 'viewers', title: 'Stats.GroupViewers' },
|
||||
{ name: 'posters', title: 'Stats.GroupPosters' },
|
||||
],
|
||||
];
|
||||
|
||||
export type OwnProps = {
|
||||
isGroup?: boolean;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
||||
};
|
||||
|
||||
const StatisticsOverview: FC<OwnProps> = ({ isGroup, statistics }) => {
|
||||
const lang = useLang();
|
||||
|
||||
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
|
||||
if (!change) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isChangeNegative = Number(change) < 0;
|
||||
|
||||
return (
|
||||
<span className={buildClassName('StatisticsOverview--value', isChangeNegative && 'negative')}>
|
||||
{isChangeNegative ? change : `+${change}`}
|
||||
<span className={buildClassName('StatisticsOverview__value', isChangeNegative && 'negative')}>
|
||||
{isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`}
|
||||
{percentage && (
|
||||
<>
|
||||
{' '}
|
||||
@ -30,40 +65,48 @@ const StatisticsOverview: FC<OwnProps> = ({ statistics }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
followers, viewsPerPost, sharesPerPost, enabledNotifications,
|
||||
} = statistics;
|
||||
const { period } = (statistics as ApiGroupStatistics);
|
||||
|
||||
return (
|
||||
<div className="StatisticsOverview">
|
||||
<h2 className="StatisticsOverview--title">{lang('ChannelStats.Overview')}</h2>
|
||||
<div className="StatisticsOverview__header">
|
||||
<div className="StatisticsOverview__title">{lang('ChannelStats.Overview')}</div>
|
||||
|
||||
<table className="StatisticsOverview--table">
|
||||
<tr>
|
||||
<td>
|
||||
<b className="StatisticsOverview--table-value">{followers.current}</b> {renderOverviewItemValue(followers)}
|
||||
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.Followers')}</h3>
|
||||
</td>
|
||||
<td>
|
||||
<b className="StatisticsOverview--table-value">{enabledNotifications.percentage}%</b>
|
||||
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.EnabledNotifications')}</h3>
|
||||
</td>
|
||||
</tr>
|
||||
{period && (
|
||||
<div className="StatisticsOverview__caption">
|
||||
{formatFullDate(lang, period.minDate * 1000)} — {formatFullDate(lang, period.maxDate * 1000)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<b className="StatisticsOverview--table-value">{viewsPerPost.current}</b>
|
||||
{' '}
|
||||
{renderOverviewItemValue(viewsPerPost)}
|
||||
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.ViewsPerPost')}</h3>
|
||||
</td>
|
||||
<td>
|
||||
<b className="StatisticsOverview--table-value">{sharesPerPost.current}</b>
|
||||
{' '}
|
||||
{renderOverviewItemValue(sharesPerPost)}
|
||||
<h3 className="StatisticsOverview--table-heading">{lang('ChannelStats.Overview.SharesPerPost')}</h3>
|
||||
</td>
|
||||
</tr>
|
||||
<table className="StatisticsOverview__table">
|
||||
{(isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
|
||||
<tr>
|
||||
{row.map((cell: OverviewCell) => {
|
||||
const field = (statistics as any)[cell.name];
|
||||
|
||||
if (cell.isPercentage) {
|
||||
return (
|
||||
<td>
|
||||
<b className="StatisticsOverview__table-value">{field.percentage}%</b>
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td>
|
||||
<b className="StatisticsOverview__table-value">
|
||||
{formatIntegerCompact(field.current)}
|
||||
</b>
|
||||
{' '}
|
||||
{renderOverviewItemValue(field)}
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,14 +2,14 @@
|
||||
position: relative;
|
||||
padding-left: 3rem;
|
||||
|
||||
&--summary {
|
||||
&__summary {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 0.75rem;
|
||||
|
||||
.media-preview--image {
|
||||
.media-preview__image {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
position: absolute;
|
||||
@ -35,24 +35,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--title {
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
&--info {
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
color: var(--color-text-meta);
|
||||
}
|
||||
|
||||
&--meta {
|
||||
&__meta {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
&--date {
|
||||
&__date {
|
||||
flex: 1;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
@ -30,20 +30,20 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
||||
|
||||
return (
|
||||
<p className="StatisticsRecentMessage">
|
||||
<div className="StatisticsRecentMessage--title">
|
||||
<div className="StatisticsRecentMessage--summary">
|
||||
<div className="StatisticsRecentMessage__title">
|
||||
<div className="StatisticsRecentMessage__summary">
|
||||
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||
</div>
|
||||
<div className="StatisticsRecentMessage--meta">
|
||||
<div className="StatisticsRecentMessage__meta">
|
||||
{lang('ChannelStats.ViewsCount', message.views)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="StatisticsRecentMessage--info">
|
||||
<div className="StatisticsRecentMessage--date">
|
||||
<div className="StatisticsRecentMessage__info">
|
||||
<div className="StatisticsRecentMessage__date">
|
||||
{formatDateTimeToString(message.date * 1000, lang.code)}
|
||||
</div>
|
||||
<div className="StatisticsRecentMessage--meta">
|
||||
<div className="StatisticsRecentMessage__meta">
|
||||
{message.forwards ? lang('ChannelStats.SharesCount', message.forwards) : 'No shares'}
|
||||
</div>
|
||||
</div>
|
||||
@ -58,7 +58,7 @@ function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRo
|
||||
|
||||
return (
|
||||
<span className="media-preview">
|
||||
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
||||
<img src={blobUrl} alt="" className={buildClassName('media-preview__image', isRoundVideo && 'round')} />
|
||||
{getMessageVideo(message) && <i className="icon-play" />}
|
||||
{renderMessageSummary(lang, message, true)}
|
||||
</span>
|
||||
|
@ -1,27 +1,28 @@
|
||||
import { addActionHandler, getGlobal } from '../../index';
|
||||
|
||||
import { ApiChannelStatistics } from '../../../api/types';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { updateStatistics, updateStatisticsGraph } from '../../reducers';
|
||||
import { selectChatMessages, selectChat } from '../../selectors';
|
||||
|
||||
addActionHandler('loadStatistics', async (global, actions, payload) => {
|
||||
const { chatId } = payload;
|
||||
const { chatId, isGroup } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat?.fullInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = await callApi('fetchStatistics', { chat });
|
||||
const result = await callApi(isGroup ? 'fetchGroupStatistics' : 'fetchChannelStatistics', { chat });
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
if (result?.recentTopMessages.length) {
|
||||
if ((result as ApiChannelStatistics).recentTopMessages?.length) {
|
||||
const messages = selectChatMessages(global, chatId);
|
||||
|
||||
result.recentTopMessages = result.recentTopMessages
|
||||
(result as ApiChannelStatistics).recentTopMessages = (result as ApiChannelStatistics).recentTopMessages
|
||||
.map((message) => ({ ...message, ...messages[message.msgId] }));
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ addActionHandler('openChat', (global, actions, payload) => {
|
||||
|
||||
global = {
|
||||
...global,
|
||||
isStatisticsShown: false,
|
||||
messages: {
|
||||
...global.messages,
|
||||
contentToBeScheduled: undefined,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { GlobalState } from '../types';
|
||||
import { ApiStatistics, StatisticsGraph } from '../../api/types';
|
||||
import { ApiChannelStatistics, ApiGroupStatistics, StatisticsGraph } from '../../api/types';
|
||||
|
||||
export function updateStatistics(
|
||||
global: GlobalState, chatId: string, statistics: ApiStatistics,
|
||||
global: GlobalState, chatId: string, statistics: ApiChannelStatistics | ApiGroupStatistics,
|
||||
): GlobalState {
|
||||
return {
|
||||
...global,
|
||||
|
@ -26,7 +26,8 @@ import {
|
||||
ApiAvailableReaction,
|
||||
ApiAppConfig,
|
||||
ApiSponsoredMessage,
|
||||
ApiStatistics,
|
||||
ApiChannelStatistics,
|
||||
ApiGroupStatistics,
|
||||
ApiPaymentFormNativeParams, ApiUpdate,
|
||||
} from '../api/types';
|
||||
import {
|
||||
@ -508,7 +509,7 @@ export type GlobalState = {
|
||||
serviceNotifications: ServiceNotification[];
|
||||
|
||||
statistics: {
|
||||
byChatId: Record<string, ApiStatistics>;
|
||||
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
|
||||
};
|
||||
|
||||
newContact?: {
|
||||
|
@ -1170,4 +1170,5 @@ langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<strin
|
||||
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.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
|
||||
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;`;
|
@ -211,5 +211,6 @@
|
||||
"messages.setDefaultReaction",
|
||||
"help.getAppConfig",
|
||||
"stats.getBroadcastStats",
|
||||
"stats.getMegagroupStats",
|
||||
"stats.loadAsyncGraph"
|
||||
]
|
||||
|
@ -152,7 +152,7 @@ function create(container, originalData) {
|
||||
}
|
||||
|
||||
function _onFocus(focusOn) {
|
||||
if (_data.isBars || _data.isPie) {
|
||||
if (_data.isBars || _data.isPie || _data.isSteps) {
|
||||
// TODO animate
|
||||
_stateManager.update({ focusOn });
|
||||
}
|
||||
|
@ -143,7 +143,6 @@ function calculateYRanges(data, filter, labelFromIndex, labelToIndex, prevState)
|
||||
const yRanges = calculateYRangesForGroup(data, labelFromIndex, labelToIndex, prevState, filteredDatasets);
|
||||
|
||||
if (secondaryYAxisDataset) {
|
||||
const group = filter[secondaryYAxisDataset.key] ? [secondaryYAxisDataset] : [];
|
||||
const {
|
||||
yMinViewport: yMinViewportSecond,
|
||||
yMaxViewport: yMaxViewportSecond,
|
||||
|
@ -408,7 +408,7 @@ export function createTooltip(container, data, plotSize, colors, onZoom, onFocus
|
||||
}
|
||||
});
|
||||
|
||||
if (data.isBars && data.isStacked) {
|
||||
if ((data.isBars || data.isSteps) && data.isStacked) {
|
||||
_renderTotal(dataSetContainer, formatInteger(totalValue));
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ export function analyzeData(data) {
|
||||
onZoom,
|
||||
isLines: data.type === 'line',
|
||||
isBars: data.type === 'bar',
|
||||
isSteps: data.type === 'step',
|
||||
isAreas: data.type === 'area',
|
||||
isPie: data.type === 'pie',
|
||||
yMin: totalYMin,
|
||||
|
@ -58,14 +58,14 @@ export function drawDatasets(
|
||||
drawDataset(datasetType, context, datasetPoints, datasetProjection, options);
|
||||
});
|
||||
|
||||
if (state.focusOn && data.isBars) {
|
||||
if (state.focusOn && (data.isBars || data.isSteps)) {
|
||||
const [x0] = toPixels(projection, 0, 0);
|
||||
const [x1] = toPixels(projection, 1, 0);
|
||||
|
||||
drawBarsMask(context, projection, {
|
||||
focusOn: state.focusOn,
|
||||
color: getCssColor(colors, 'mask'),
|
||||
lineWidth: x1 - x0,
|
||||
lineWidth: data.isSteps ? x1 - x0 + lineWidth : x1 - x0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,8 @@ function drawDataset(type, ...args) {
|
||||
return drawDatasetLine(...args);
|
||||
case 'bar':
|
||||
return drawDatasetBars(...args);
|
||||
case 'step':
|
||||
return drawDatasetSteps(...args);
|
||||
case 'area':
|
||||
return drawDatasetArea(...args);
|
||||
case 'pie':
|
||||
@ -138,6 +140,31 @@ function drawDatasetBars(context, points, projection, options) {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawDatasetSteps(context, points, projection, options) {
|
||||
context.beginPath();
|
||||
|
||||
let pixels = [];
|
||||
|
||||
for (let j = 0, l = points.length; j < l; j++) {
|
||||
const { labelIndex, stackValue } = points[j];
|
||||
pixels.push(
|
||||
toPixels(projection, labelIndex - PLOT_BARS_WIDTH_SHIFT, stackValue),
|
||||
toPixels(projection, labelIndex + PLOT_BARS_WIDTH_SHIFT, stackValue),
|
||||
);
|
||||
}
|
||||
|
||||
pixels.forEach(([x, y]) => {
|
||||
context.lineTo(x, y);
|
||||
});
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = options.color;
|
||||
context.lineWidth = options.lineWidth;
|
||||
context.globalAlpha = options.opacity;
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawBarsMask(context, projection, options) {
|
||||
const [xCenter, yCenter] = projection.getCenter();
|
||||
const [width, height] = projection.getSize();
|
||||
|
@ -17,7 +17,7 @@
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
margin-right: 2em;
|
||||
text-transform: lowercase;
|
||||
|
||||
&:first-letter {
|
||||
|
Loading…
x
Reference in New Issue
Block a user