Statistics: Fix statistics overview (#5963)
This commit is contained in:
parent
f4240a93ab
commit
fb79e088fe
@ -8,12 +8,12 @@ import type {
|
||||
ApiPostStatistics,
|
||||
ApiStoryPublicForward,
|
||||
ChannelMonetizationBalances,
|
||||
StatisticsGraph,
|
||||
StatisticsMessageInteractionCounter,
|
||||
StatisticsOverviewItem,
|
||||
StatisticsOverviewPercentage,
|
||||
StatisticsOverviewPeriod,
|
||||
StatisticsStoryInteractionCounter,
|
||||
TypeStatisticsGraph,
|
||||
} from '../../types';
|
||||
|
||||
import { buildApiUsernames, buildAvatarPhotoId } from './common';
|
||||
@ -23,20 +23,19 @@ const DECIMALS = 10 ** 9;
|
||||
|
||||
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics {
|
||||
return {
|
||||
type: 'channel',
|
||||
// Graphs
|
||||
growthGraph: buildGraph(stats.growthGraph),
|
||||
followersGraph: buildGraph(stats.followersGraph),
|
||||
muteGraph: buildGraph(stats.muteGraph),
|
||||
topHoursGraph: buildGraph(stats.topHoursGraph),
|
||||
|
||||
// Async graphs
|
||||
languagesGraph: (stats.languagesGraph as GramJs.StatsGraphAsync).token,
|
||||
viewsBySourceGraph: (stats.viewsBySourceGraph as GramJs.StatsGraphAsync).token,
|
||||
newFollowersBySourceGraph: (stats.newFollowersBySourceGraph as GramJs.StatsGraphAsync).token,
|
||||
interactionsGraph: (stats.interactionsGraph as GramJs.StatsGraphAsync).token,
|
||||
reactionsByEmotionGraph: (stats.reactionsByEmotionGraph as GramJs.StatsGraphAsync).token,
|
||||
storyInteractionsGraph: (stats.storyInteractionsGraph as GramJs.StatsGraphAsync).token,
|
||||
storyReactionsByEmotionGraph: (stats.storyReactionsByEmotionGraph as GramJs.StatsGraphAsync).token,
|
||||
languagesGraph: buildGraph(stats.languagesGraph),
|
||||
viewsBySourceGraph: buildGraph(stats.viewsBySourceGraph),
|
||||
newFollowersBySourceGraph: buildGraph(stats.newFollowersBySourceGraph),
|
||||
interactionsGraph: buildGraph(stats.interactionsGraph),
|
||||
reactionsByEmotionGraph: buildGraph(stats.reactionsByEmotionGraph),
|
||||
storyInteractionsGraph: buildGraph(stats.storyInteractionsGraph),
|
||||
storyReactionsByEmotionGraph: buildGraph(stats.storyReactionsByEmotionGraph),
|
||||
|
||||
// Statistics overview
|
||||
followers: buildStatisticsOverview(stats.followers),
|
||||
@ -72,6 +71,7 @@ export function buildApiPostInteractionCounter(
|
||||
): StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter | undefined {
|
||||
if (interaction instanceof GramJs.PostInteractionCountersMessage) {
|
||||
return {
|
||||
type: 'message',
|
||||
msgId: interaction.msgId,
|
||||
forwardsCount: interaction.forwards,
|
||||
viewsCount: interaction.views,
|
||||
@ -81,6 +81,7 @@ export function buildApiPostInteractionCounter(
|
||||
|
||||
if (interaction instanceof GramJs.PostInteractionCountersStory) {
|
||||
return {
|
||||
type: 'story',
|
||||
storyId: interaction.storyId,
|
||||
reactionsCount: interaction.reactions,
|
||||
viewsCount: interaction.views,
|
||||
@ -93,15 +94,14 @@ export function buildApiPostInteractionCounter(
|
||||
|
||||
export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGroupStatistics {
|
||||
return {
|
||||
type: 'group',
|
||||
// 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,
|
||||
languagesGraph: buildGraph(stats.languagesGraph),
|
||||
messagesGraph: buildGraph(stats.messagesGraph),
|
||||
actionsGraph: buildGraph(stats.actionsGraph),
|
||||
|
||||
// Statistics overview
|
||||
period: getOverviewPeriod(stats.period),
|
||||
@ -158,18 +158,29 @@ export function buildStoryPublicForwards(
|
||||
|
||||
export function buildGraph(
|
||||
result: GramJs.TypeStatsGraph, isPercentage?: boolean, isCurrency?: boolean, currencyRate?: number,
|
||||
): StatisticsGraph | undefined {
|
||||
if ((result as GramJs.StatsGraphError).error) {
|
||||
return undefined;
|
||||
): TypeStatisticsGraph {
|
||||
if (result instanceof GramJs.StatsGraphError) {
|
||||
return {
|
||||
graphType: 'error',
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
|
||||
const data = JSON.parse((result as GramJs.StatsGraph).json.data);
|
||||
if (result instanceof GramJs.StatsGraphAsync) {
|
||||
return {
|
||||
graphType: 'async',
|
||||
token: result.token,
|
||||
};
|
||||
}
|
||||
|
||||
const data = JSON.parse(result.json.data);
|
||||
const [x, ...y] = data.columns;
|
||||
const hasSecondYAxis = data.y_scaled;
|
||||
|
||||
return {
|
||||
graphType: 'graph',
|
||||
type: isPercentage ? 'area' : data.types.y0,
|
||||
zoomToken: (result as GramJs.StatsGraph).zoomToken,
|
||||
zoomToken: result.zoomToken,
|
||||
labelFormatter: data.xTickFormatter,
|
||||
tooltipFormatter: data.xTooltipFormatter,
|
||||
labels: x.slice(1),
|
||||
|
||||
@ -270,6 +270,30 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
|
||||
return { message };
|
||||
}
|
||||
|
||||
export async function fetchMessagesById({ chat, messageIds }: { chat: ApiChat; messageIds: number[] }) {
|
||||
const isChannel = getEntityTypeById(chat.id) === 'channel';
|
||||
|
||||
const result = await invokeRequest(
|
||||
isChannel
|
||||
? new GramJs.channels.GetMessages({
|
||||
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||
id: messageIds.map((id) => new GramJs.InputMessageID({ id })),
|
||||
})
|
||||
: new GramJs.messages.GetMessages({
|
||||
id: messageIds.map((id) => new GramJs.InputMessageID({ id })),
|
||||
}),
|
||||
{
|
||||
shouldThrow: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!result || result instanceof GramJs.messages.MessagesNotModified) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.messages.map(buildApiMessage).filter(Boolean);
|
||||
}
|
||||
|
||||
let mediaQueue = Promise.resolve();
|
||||
|
||||
export function sendMessageLocal(
|
||||
|
||||
@ -5,7 +5,6 @@ import type {
|
||||
ApiChat, ApiMessagePublicForward, ApiPeer, ApiPostStatistics, ApiStoryPublicForward, StatisticsGraph,
|
||||
} from '../../types';
|
||||
|
||||
import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config';
|
||||
import {
|
||||
buildChannelMonetizationStatistics,
|
||||
buildChannelStatistics,
|
||||
@ -104,11 +103,13 @@ export async function fetchMessagePublicForwards({
|
||||
messageId,
|
||||
dcId,
|
||||
offset = DEFAULT_PRIMITIVES.STRING,
|
||||
limit = DEFAULT_PRIMITIVES.INT,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
dcId?: number;
|
||||
offset?: string;
|
||||
limit?: number;
|
||||
}): Promise<{
|
||||
forwards?: ApiMessagePublicForward[];
|
||||
count?: number;
|
||||
@ -118,7 +119,7 @@ export async function fetchMessagePublicForwards({
|
||||
channel: buildInputChannel(chat.id, chat.accessHash),
|
||||
msgId: messageId,
|
||||
offset,
|
||||
limit: STATISTICS_PUBLIC_FORWARDS_LIMIT,
|
||||
limit,
|
||||
}), {
|
||||
dcId,
|
||||
});
|
||||
@ -156,7 +157,10 @@ export async function fetchStatisticsAsyncGraph({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildGraph(result as GramJs.StatsGraph, isPercentage);
|
||||
const graph = buildGraph(result, isPercentage);
|
||||
|
||||
if (graph.graphType !== 'graph') return undefined;
|
||||
return graph;
|
||||
}
|
||||
|
||||
export async function fetchStoryStatistics({
|
||||
@ -187,11 +191,13 @@ export async function fetchStoryPublicForwards({
|
||||
storyId,
|
||||
dcId,
|
||||
offset = DEFAULT_PRIMITIVES.STRING,
|
||||
limit = DEFAULT_PRIMITIVES.INT,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
storyId: number;
|
||||
dcId?: number;
|
||||
offset?: string;
|
||||
limit?: number;
|
||||
}): Promise<{
|
||||
publicForwards: (ApiMessagePublicForward | ApiStoryPublicForward)[] | undefined;
|
||||
count?: number;
|
||||
@ -201,7 +207,7 @@ export async function fetchStoryPublicForwards({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: storyId,
|
||||
offset,
|
||||
limit: STATISTICS_PUBLIC_FORWARDS_LIMIT,
|
||||
limit,
|
||||
}), {
|
||||
dcId,
|
||||
});
|
||||
|
||||
@ -2,17 +2,18 @@ import type { ApiChat } from './chats';
|
||||
import type { ApiTypePrepaidGiveaway } from './payments';
|
||||
|
||||
export interface ApiChannelStatistics {
|
||||
growthGraph?: StatisticsGraph | string;
|
||||
followersGraph?: StatisticsGraph | string;
|
||||
muteGraph?: StatisticsGraph | string;
|
||||
topHoursGraph?: StatisticsGraph | string;
|
||||
reactionsByEmotionGraph?: StatisticsGraph | string;
|
||||
storyInteractionsGraph?: StatisticsGraph | string;
|
||||
storyReactionsByEmotionGraph?: StatisticsGraph | string;
|
||||
interactionsGraph: StatisticsGraph | string;
|
||||
viewsBySourceGraph: StatisticsGraph | string;
|
||||
newFollowersBySourceGraph: StatisticsGraph | string;
|
||||
languagesGraph: StatisticsGraph | string;
|
||||
type: 'channel';
|
||||
growthGraph?: TypeStatisticsGraph;
|
||||
followersGraph?: TypeStatisticsGraph;
|
||||
muteGraph?: TypeStatisticsGraph;
|
||||
topHoursGraph?: TypeStatisticsGraph;
|
||||
reactionsByEmotionGraph?: TypeStatisticsGraph;
|
||||
storyInteractionsGraph?: TypeStatisticsGraph;
|
||||
storyReactionsByEmotionGraph?: TypeStatisticsGraph;
|
||||
interactionsGraph: TypeStatisticsGraph;
|
||||
viewsBySourceGraph: TypeStatisticsGraph;
|
||||
newFollowersBySourceGraph: TypeStatisticsGraph;
|
||||
languagesGraph: TypeStatisticsGraph;
|
||||
followers: StatisticsOverviewItem;
|
||||
viewsPerPost: StatisticsOverviewItem;
|
||||
sharesPerPost: StatisticsOverviewItem;
|
||||
@ -25,19 +26,20 @@ export interface ApiChannelStatistics {
|
||||
}
|
||||
|
||||
export interface ApiChannelMonetizationStatistics {
|
||||
topHoursGraph?: StatisticsGraph | string;
|
||||
revenueGraph?: StatisticsGraph | string;
|
||||
topHoursGraph?: TypeStatisticsGraph;
|
||||
revenueGraph?: TypeStatisticsGraph;
|
||||
balances?: ChannelMonetizationBalances;
|
||||
usdRate?: number;
|
||||
}
|
||||
|
||||
export interface ApiGroupStatistics {
|
||||
growthGraph?: StatisticsGraph | string;
|
||||
membersGraph?: StatisticsGraph | string;
|
||||
topHoursGraph?: StatisticsGraph | string;
|
||||
languagesGraph: StatisticsGraph | string;
|
||||
messagesGraph: StatisticsGraph | string;
|
||||
actionsGraph: StatisticsGraph | string;
|
||||
type: 'group';
|
||||
growthGraph?: TypeStatisticsGraph;
|
||||
membersGraph?: TypeStatisticsGraph;
|
||||
topHoursGraph?: TypeStatisticsGraph;
|
||||
languagesGraph: TypeStatisticsGraph;
|
||||
messagesGraph: TypeStatisticsGraph;
|
||||
actionsGraph: TypeStatisticsGraph;
|
||||
period: StatisticsOverviewPeriod;
|
||||
members: StatisticsOverviewItem;
|
||||
viewers: StatisticsOverviewItem;
|
||||
@ -46,8 +48,8 @@ export interface ApiGroupStatistics {
|
||||
}
|
||||
|
||||
export interface ApiPostStatistics {
|
||||
viewsGraph?: StatisticsGraph | string;
|
||||
reactionsGraph?: StatisticsGraph | string;
|
||||
viewsGraph?: TypeStatisticsGraph;
|
||||
reactionsGraph?: TypeStatisticsGraph;
|
||||
forwardsCount?: number;
|
||||
viewsCount?: number;
|
||||
reactionsCount?: number;
|
||||
@ -80,6 +82,7 @@ export interface ApiStoryPublicForward {
|
||||
}
|
||||
|
||||
export interface StatisticsGraph {
|
||||
graphType: 'graph';
|
||||
type: string;
|
||||
zoomToken?: string;
|
||||
labelFormatter: string;
|
||||
@ -104,6 +107,18 @@ export interface StatisticsGraph {
|
||||
};
|
||||
}
|
||||
|
||||
export interface StatisticsGraphError {
|
||||
graphType: 'error';
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface StatisticsGraphAsync {
|
||||
graphType: 'async';
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type TypeStatisticsGraph = StatisticsGraph | StatisticsGraphError | StatisticsGraphAsync;
|
||||
|
||||
export interface StatisticsOverviewItem {
|
||||
current?: number;
|
||||
change?: number;
|
||||
@ -122,6 +137,7 @@ export interface StatisticsOverviewPeriod {
|
||||
}
|
||||
|
||||
export interface StatisticsMessageInteractionCounter {
|
||||
type: 'message';
|
||||
msgId: number;
|
||||
forwardsCount: number;
|
||||
viewsCount: number;
|
||||
@ -129,6 +145,7 @@ export interface StatisticsMessageInteractionCounter {
|
||||
}
|
||||
|
||||
export interface StatisticsStoryInteractionCounter {
|
||||
type: 'story';
|
||||
storyId: number;
|
||||
viewsCount: number;
|
||||
forwardsCount: number;
|
||||
|
||||
@ -1026,7 +1026,11 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('quick-reaction', isQuickReactionVisible && !hasActiveReactions && 'visible')}
|
||||
className={buildClassName(
|
||||
'quick-reaction',
|
||||
'no-selection',
|
||||
isQuickReactionVisible && !hasActiveReactions && 'visible',
|
||||
)}
|
||||
onClick={handleSendQuickReaction}
|
||||
ref={quickReactionRef}
|
||||
>
|
||||
|
||||
@ -6,14 +6,13 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type {
|
||||
ApiMessagePublicForward,
|
||||
ApiPostStatistics,
|
||||
StatisticsGraph,
|
||||
} from '../../../api/types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config';
|
||||
import { selectChatFullInfo, selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { isGraph } from './helpers/isGraph';
|
||||
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
@ -56,7 +55,7 @@ export type StateProps = {
|
||||
dcId?: number;
|
||||
};
|
||||
|
||||
function Statistics({
|
||||
function MessageStatistics({
|
||||
chatId,
|
||||
isActive,
|
||||
statistics,
|
||||
@ -66,7 +65,8 @@ function Statistics({
|
||||
const lang = useOldLang();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const loadedCharts = useRef<string[]>([]);
|
||||
const loadedCharts = useRef<Set<string>>(new Set());
|
||||
const errorCharts = useRef<Set<string>>(new Set());
|
||||
|
||||
const { loadMessageStatistics, loadMessagePublicForwards, loadStatisticsAsyncGraph } = getActions();
|
||||
const forceUpdate = useForceUpdate();
|
||||
@ -79,7 +79,8 @@ function Statistics({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || messageId) {
|
||||
loadedCharts.current = [];
|
||||
loadedCharts.current.clear();
|
||||
errorCharts.current.clear();
|
||||
setIsReady(false);
|
||||
}
|
||||
}, [isActive, messageId]);
|
||||
@ -92,10 +93,13 @@ function Statistics({
|
||||
|
||||
GRAPHS.forEach((name) => {
|
||||
const graph = statistics[name];
|
||||
const isAsync = typeof graph === 'string';
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
const isAsync = graph.graphType === 'async';
|
||||
|
||||
if (isAsync) {
|
||||
loadStatisticsAsyncGraph({ name, chatId, token: graph });
|
||||
loadStatisticsAsyncGraph({ name, chatId, token: graph.token });
|
||||
}
|
||||
});
|
||||
}, [chatId, statistics, loadStatisticsAsyncGraph]);
|
||||
@ -115,19 +119,24 @@ function Statistics({
|
||||
|
||||
GRAPHS.forEach((name, index: number) => {
|
||||
const graph = statistics[name];
|
||||
const isAsync = typeof graph === 'string';
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
const isAsync = graph.graphType === 'async';
|
||||
const isError = graph.graphType === 'error';
|
||||
|
||||
if (isAsync || loadedCharts.current.includes(name)) {
|
||||
if (isAsync || loadedCharts.current.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph) {
|
||||
loadedCharts.current.push(name);
|
||||
if (isError) {
|
||||
loadedCharts.current.add(name);
|
||||
errorCharts.current.add(name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { zoomToken } = graph as StatisticsGraph;
|
||||
const { zoomToken } = graph;
|
||||
|
||||
LovelyChart.create(
|
||||
containerRef.current!.children[index] as HTMLElement,
|
||||
@ -137,11 +146,11 @@ function Statistics({
|
||||
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
|
||||
zoomOutLabel: lang('Graph.ZoomOut'),
|
||||
} : {},
|
||||
...graph as StatisticsGraph,
|
||||
...graph,
|
||||
},
|
||||
);
|
||||
|
||||
loadedCharts.current.push(name);
|
||||
loadedCharts.current.add(name);
|
||||
});
|
||||
|
||||
forceUpdate();
|
||||
@ -161,15 +170,21 @@ function Statistics({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
|
||||
<div
|
||||
key={`${chatId}-${messageId}`}
|
||||
className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}
|
||||
>
|
||||
<StatisticsOverview statistics={statistics} type="message" title={lang('StatisticOverview')} />
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
{(!loadedCharts.current.size || !statistics.publicForwardsData) && <Loading />}
|
||||
|
||||
<div ref={containerRef}>
|
||||
{GRAPHS.map((graph) => (
|
||||
<div className={buildClassName(styles.graph, !loadedCharts.current.includes(graph) && styles.hidden)} />
|
||||
))}
|
||||
{GRAPHS.map((graph) => {
|
||||
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
|
||||
return (
|
||||
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{Boolean(statistics.publicForwards) && (
|
||||
@ -180,7 +195,6 @@ function Statistics({
|
||||
items={statistics.publicForwardsData}
|
||||
itemSelector=".statistic-public-forward"
|
||||
onLoadMore={handleLoadMore}
|
||||
preloadBackwards={STATISTICS_PUBLIC_FORWARDS_LIMIT}
|
||||
noFastList
|
||||
>
|
||||
{(statistics.publicForwardsData as ApiMessagePublicForward[]).map((item) => (
|
||||
@ -202,4 +216,4 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return { statistics, dcId, messageId };
|
||||
},
|
||||
)(Statistics));
|
||||
)(MessageStatistics));
|
||||
|
||||
@ -3,11 +3,12 @@ import {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChannelMonetizationStatistics, StatisticsGraph } from '../../../api/types';
|
||||
import type { ApiChannelMonetizationStatistics } from '../../../api/types';
|
||||
|
||||
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { isGraph } from './helpers/isGraph';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
@ -68,7 +69,9 @@ const MonetizationStatistics = ({
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const loadedCharts = useRef<string[]>([]);
|
||||
const loadedCharts = useRef<Set<string>>(new Set());
|
||||
const errorCharts = useRef<Set<string>>(new Set());
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
const [isAboutMonetizationModalOpen, openAboutMonetizationModal, closeAboutMonetizationModal] = useFlag(false);
|
||||
const [isConfirmPasswordDialogOpen, openConfirmPasswordDialogOpen, closeConfirmPasswordDialogOpen] = useFlag();
|
||||
@ -100,7 +103,8 @@ const MonetizationStatistics = ({
|
||||
});
|
||||
}
|
||||
|
||||
loadedCharts.current = [];
|
||||
loadedCharts.current.clear();
|
||||
errorCharts.current.clear();
|
||||
|
||||
if (!statistics || !containerRef.current) {
|
||||
return;
|
||||
@ -108,24 +112,29 @@ const MonetizationStatistics = ({
|
||||
|
||||
MONETIZATION_GRAPHS.forEach((name, index: number) => {
|
||||
const graph = statistics[name];
|
||||
const isAsync = typeof graph === 'string';
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
const isAsync = graph.graphType === 'async';
|
||||
const isError = graph.graphType === 'error';
|
||||
|
||||
if (isAsync || loadedCharts.current.includes(name)) {
|
||||
if (isAsync || loadedCharts.current.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph) {
|
||||
loadedCharts.current.push(name);
|
||||
if (isError) {
|
||||
loadedCharts.current.add(name);
|
||||
errorCharts.current.add(name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LovelyChart.create(containerRef.current!.children[index] as HTMLElement, {
|
||||
title: oldLang((MONETIZATION_GRAPHS_TITLES as Record<string, string>)[name]),
|
||||
...graph as StatisticsGraph,
|
||||
...graph,
|
||||
});
|
||||
|
||||
loadedCharts.current.push(name);
|
||||
loadedCharts.current.add(name);
|
||||
|
||||
containerRef.current!.children[index].classList.remove(styles.hidden);
|
||||
});
|
||||
@ -232,7 +241,7 @@ const MonetizationStatistics = ({
|
||||
}
|
||||
/>
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
{!loadedCharts.current.size && <Loading />}
|
||||
|
||||
<div ref={containerRef} className={styles.section}>
|
||||
{MONETIZATION_GRAPHS.filter(Boolean).map((graph) => (
|
||||
|
||||
@ -36,7 +36,6 @@
|
||||
|
||||
.messages, .publicForwards {
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid var(--color-borders);
|
||||
|
||||
&-title {
|
||||
padding-left: 0.75rem;
|
||||
@ -69,6 +68,7 @@
|
||||
|
||||
&.hidden {
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import {
|
||||
memo, useEffect, useMemo,
|
||||
useRef, useState,
|
||||
@ -22,6 +21,7 @@ import {
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { isGraph } from './helpers/isGraph';
|
||||
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
@ -84,7 +84,7 @@ export type StateProps = {
|
||||
storiesById?: Record<string, ApiTypeStory>;
|
||||
};
|
||||
|
||||
const Statistics: FC<OwnProps & StateProps> = ({
|
||||
const Statistics = ({
|
||||
chatId,
|
||||
chat,
|
||||
statistics,
|
||||
@ -92,11 +92,12 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
isGroup,
|
||||
messagesById,
|
||||
storiesById,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const lang = useOldLang();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const loadedCharts = useRef<string[]>([]);
|
||||
const loadedCharts = useRef<Set<string>>(new Set());
|
||||
const errorCharts = useRef<Set<string>>(new Set());
|
||||
|
||||
const { loadStatistics, loadStatisticsAsyncGraph } = getActions();
|
||||
const forceUpdate = useForceUpdate();
|
||||
@ -121,13 +122,16 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
graphs.forEach((name) => {
|
||||
const graph = statistics[name as keyof typeof statistics];
|
||||
const isAsync = typeof graph === 'string';
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
const isAsync = graph.graphType === 'async';
|
||||
|
||||
if (isAsync) {
|
||||
loadStatisticsAsyncGraph({
|
||||
name,
|
||||
chatId,
|
||||
token: graph,
|
||||
token: graph.token,
|
||||
// Hardcode percentage for languages graph, since API does not return `percentage` flag
|
||||
isPercentage: name === 'languagesGraph',
|
||||
});
|
||||
@ -150,14 +154,20 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
graphs.forEach((name, index: number) => {
|
||||
const graph = statistics[name as keyof typeof statistics];
|
||||
const isAsync = typeof graph === 'string';
|
||||
|
||||
if (isAsync || loadedCharts.current.includes(name)) {
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph) {
|
||||
loadedCharts.current.push(name);
|
||||
const isAsync = graph.graphType === 'async';
|
||||
const isError = graph.graphType === 'error';
|
||||
|
||||
if (isAsync || loadedCharts.current.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
loadedCharts.current.add(name);
|
||||
errorCharts.current.add(name);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -176,7 +186,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
},
|
||||
);
|
||||
|
||||
loadedCharts.current.push(name);
|
||||
loadedCharts.current.add(name);
|
||||
|
||||
containerRef.current!.children[index].classList.remove(styles.hidden);
|
||||
});
|
||||
@ -197,12 +207,15 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
{!loadedCharts.current.size && <Loading />}
|
||||
|
||||
<div ref={containerRef}>
|
||||
{graphs.map((graph) => (
|
||||
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
|
||||
))}
|
||||
{graphs.map((graph) => {
|
||||
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
|
||||
return (
|
||||
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{Boolean((statistics as ApiChannelStatistics)?.recentPosts?.length) && (
|
||||
|
||||
@ -100,6 +100,8 @@ const BOOST_OVERVIEW: OverviewCell[][] = [
|
||||
|
||||
type StatisticsType = 'channel' | 'group' | 'message' | 'boost' | 'story' | 'monetization';
|
||||
|
||||
const DEFAULT_VALUE = 0;
|
||||
|
||||
export type OwnProps = {
|
||||
type: StatisticsType;
|
||||
title?: string;
|
||||
@ -212,13 +214,13 @@ const StatisticsOverview: FC<OwnProps> = ({
|
||||
) : schema.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell: OverviewCell) => {
|
||||
const field = (statistics as any)[cell.name];
|
||||
const field = (statistics as any)?.[cell.name];
|
||||
|
||||
if (cell.isPlain) {
|
||||
return (
|
||||
<td className={styles.tableCell}>
|
||||
<b className={styles.tableValue}>
|
||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
|
||||
{`${cell.isApproximate ? '≈ ' : ''}${formatInteger(field ?? DEFAULT_VALUE)}`}
|
||||
</b>
|
||||
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
|
||||
</td>
|
||||
@ -226,15 +228,18 @@ const StatisticsOverview: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
if (cell.isPercentage) {
|
||||
const part = field?.part ?? DEFAULT_VALUE;
|
||||
const percentage = field?.percentage ?? DEFAULT_VALUE;
|
||||
|
||||
return (
|
||||
<td className={styles.tableCell}>
|
||||
{cell.withAbsoluteValue && (
|
||||
<span className={styles.tableValue}>
|
||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`}
|
||||
{`${cell.isApproximate ? '≈ ' : ''}${formatInteger(part)}`}
|
||||
</span>
|
||||
)}
|
||||
<span className={cell.withAbsoluteValue ? styles.tableSecondaryValue : styles.tableValue}>
|
||||
{field.percentage}
|
||||
{percentage}
|
||||
%
|
||||
</span>
|
||||
<h3 className={styles.tableHeading}>{oldLang(cell.title)}</h3>
|
||||
@ -245,7 +250,7 @@ const StatisticsOverview: FC<OwnProps> = ({
|
||||
return (
|
||||
<td className={styles.tableCell}>
|
||||
<b className={styles.tableValue}>
|
||||
{formatIntegerCompact(lang, field.current)}
|
||||
{formatIntegerCompact(lang, field?.current ?? DEFAULT_VALUE)}
|
||||
</b>
|
||||
{' '}
|
||||
{renderOverviewItemValue(field)}
|
||||
|
||||
@ -7,13 +7,12 @@ import type {
|
||||
ApiChat,
|
||||
ApiPostStatistics,
|
||||
ApiUser,
|
||||
StatisticsGraph,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config';
|
||||
import { selectChatFullInfo, selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { isGraph } from './helpers/isGraph';
|
||||
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
@ -71,7 +70,8 @@ function StoryStatistics({
|
||||
const lang = useOldLang();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const loadedCharts = useRef<string[]>([]);
|
||||
const loadedCharts = useRef<Set<string>>(new Set());
|
||||
const errorCharts = useRef<Set<string>>(new Set());
|
||||
|
||||
const { loadStoryStatistics, loadStoryPublicForwards, loadStatisticsAsyncGraph } = getActions();
|
||||
const forceUpdate = useForceUpdate();
|
||||
@ -84,7 +84,8 @@ function StoryStatistics({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || storyId) {
|
||||
loadedCharts.current = [];
|
||||
loadedCharts.current.clear();
|
||||
errorCharts.current.clear();
|
||||
setIsReady(false);
|
||||
}
|
||||
}, [isActive, storyId]);
|
||||
@ -97,10 +98,13 @@ function StoryStatistics({
|
||||
|
||||
GRAPHS.forEach((name) => {
|
||||
const graph = statistics[name];
|
||||
const isAsync = typeof graph === 'string';
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
const isAsync = graph.graphType === 'async';
|
||||
|
||||
if (isAsync) {
|
||||
loadStatisticsAsyncGraph({ name, chatId, token: graph });
|
||||
loadStatisticsAsyncGraph({ name, chatId, token: graph.token });
|
||||
}
|
||||
});
|
||||
}, [chatId, statistics, loadStatisticsAsyncGraph]);
|
||||
@ -120,19 +124,24 @@ function StoryStatistics({
|
||||
|
||||
GRAPHS.forEach((name, index: number) => {
|
||||
const graph = statistics[name];
|
||||
const isAsync = typeof graph === 'string';
|
||||
if (!isGraph(graph)) {
|
||||
return;
|
||||
}
|
||||
const isAsync = graph.graphType === 'async';
|
||||
const isError = graph.graphType === 'error';
|
||||
|
||||
if (isAsync || loadedCharts.current.includes(name)) {
|
||||
if (isAsync || loadedCharts.current.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph) {
|
||||
loadedCharts.current.push(name);
|
||||
if (isError) {
|
||||
loadedCharts.current.add(name);
|
||||
errorCharts.current.add(name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { zoomToken } = graph as StatisticsGraph;
|
||||
const { zoomToken } = graph;
|
||||
|
||||
LovelyChart.create(
|
||||
containerRef.current!.children[index] as HTMLElement,
|
||||
@ -142,11 +151,11 @@ function StoryStatistics({
|
||||
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
|
||||
zoomOutLabel: lang('Graph.ZoomOut'),
|
||||
} : {},
|
||||
...graph as StatisticsGraph,
|
||||
...graph,
|
||||
},
|
||||
);
|
||||
|
||||
loadedCharts.current.push(name);
|
||||
loadedCharts.current.add(name);
|
||||
});
|
||||
|
||||
forceUpdate();
|
||||
@ -166,15 +175,21 @@ function StoryStatistics({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
|
||||
<div
|
||||
key={`${chatId}-${storyId}`}
|
||||
className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}
|
||||
>
|
||||
<StatisticsOverview statistics={statistics} type="story" title={lang('StatisticOverview')} />
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
{!loadedCharts.current.size && <Loading />}
|
||||
|
||||
<div ref={containerRef}>
|
||||
{GRAPHS.map((graph) => (
|
||||
<div className={buildClassName(styles.graph, !loadedCharts.current.includes(graph) && styles.hidden)} />
|
||||
))}
|
||||
{GRAPHS.map((graph) => {
|
||||
const isReady = loadedCharts.current.has(graph) && !errorCharts.current.has(graph);
|
||||
return (
|
||||
<div className={buildClassName(styles.graph, !isReady && styles.hidden)} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{Boolean(statistics.publicForwards) && (
|
||||
@ -185,7 +200,6 @@ function StoryStatistics({
|
||||
items={statistics.publicForwardsData}
|
||||
itemSelector=".statistic-public-forward"
|
||||
onLoadMore={handleLoadMore}
|
||||
preloadBackwards={STATISTICS_PUBLIC_FORWARDS_LIMIT}
|
||||
noFastList
|
||||
>
|
||||
{statistics.publicForwardsData!.map((item) => {
|
||||
|
||||
6
src/components/right/statistics/helpers/isGraph.ts
Normal file
6
src/components/right/statistics/helpers/isGraph.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { TypeStatisticsGraph } from '../../../../api/types';
|
||||
|
||||
export function isGraph(obj: unknown): obj is TypeStatisticsGraph {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return typeof obj === 'object' && obj !== null && 'graphType' in obj;
|
||||
}
|
||||
@ -104,7 +104,6 @@ export const TOPIC_LIST_SENSITIVE_AREA = 600;
|
||||
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
||||
export const STORY_LIST_LIMIT = 100;
|
||||
export const API_GENERAL_ID_LIMIT = 100;
|
||||
export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50;
|
||||
export const RESALE_GIFTS_LIMIT = 50;
|
||||
export const TODO_ITEMS_LIMIT = 30;
|
||||
export const TODO_TITLE_LENGTH_LIMIT = 32;
|
||||
|
||||
@ -314,6 +314,24 @@ addActionHandler('loadMessage', async (global, actions, payload): Promise<void>
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('loadMessagesById', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, messageIds } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = await callApi('fetchMessagesById', {
|
||||
chat,
|
||||
messageIds,
|
||||
});
|
||||
if (!messages) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = addChatMessagesById(global, chatId, buildCollectionByKey(messages, 'id'));
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('sendMessage', async (global, actions, payload): Promise<void> => {
|
||||
const { messageList, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
@ -39,6 +38,25 @@ addActionHandler('loadStatistics', async (global, actions, payload): Promise<voi
|
||||
global = getGlobal();
|
||||
global = updateStatistics(global, chatId, stats, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
if (stats.type === 'channel') {
|
||||
const messageInteractions = stats.recentPosts.filter((post) => post.type === 'message');
|
||||
const storyInteractions = stats.recentPosts.filter((post) => post.type === 'story');
|
||||
|
||||
if (messageInteractions.length > 0) {
|
||||
actions.loadMessagesById({
|
||||
chatId,
|
||||
messageIds: messageInteractions.map((interaction) => interaction.msgId),
|
||||
});
|
||||
}
|
||||
|
||||
if (storyInteractions.length > 0) {
|
||||
actions.loadPeerStoriesByIds({
|
||||
peerId: chatId,
|
||||
storyIds: storyInteractions.map((interaction) => interaction.storyId),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('loadChannelMonetizationStatistics', async (global, actions, payload): Promise<void> => {
|
||||
@ -122,17 +140,11 @@ addActionHandler('loadMessagePublicForwards', async (global, actions, payload):
|
||||
count,
|
||||
} = publicForwards || {};
|
||||
|
||||
// Api returns the last element from the previous page as the first element
|
||||
const shouldOmitFirstElement = stats.publicForwardsData?.length && forwards?.length
|
||||
&& areDeepEqual(stats.publicForwardsData[stats.publicForwardsData.length - 1], forwards[0]);
|
||||
|
||||
global = getGlobal();
|
||||
global = updateMessageStatistics(global, {
|
||||
...stats,
|
||||
publicForwards: count || forwards?.length,
|
||||
publicForwardsData: (stats.publicForwardsData || []).concat(
|
||||
shouldOmitFirstElement ? forwards.slice(1) : (forwards || []),
|
||||
),
|
||||
publicForwardsData: (stats.publicForwardsData || []).concat((forwards || [])),
|
||||
nextOffset,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
@ -184,7 +184,9 @@ addActionHandler('toggleMessageStatistics', (global, actions, payload): ActionRe
|
||||
statistics: {
|
||||
...selectTabState(global, tabId).statistics,
|
||||
currentMessageId: messageId,
|
||||
currentMessage: undefined,
|
||||
currentStoryId: undefined,
|
||||
currentStory: undefined,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
@ -196,6 +198,8 @@ addActionHandler('toggleStoryStatistics', (global, actions, payload): ActionRetu
|
||||
...selectTabState(global, tabId).statistics,
|
||||
currentStoryId: storyId,
|
||||
currentMessageId: undefined,
|
||||
currentMessage: undefined,
|
||||
currentStory: undefined,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -504,6 +504,10 @@ export interface ActionPayloads {
|
||||
isDeleting?: boolean;
|
||||
};
|
||||
};
|
||||
loadMessagesById: {
|
||||
chatId: string;
|
||||
messageIds: number[];
|
||||
};
|
||||
editMessage: {
|
||||
messageList?: MessageList;
|
||||
text: string;
|
||||
@ -1570,7 +1574,7 @@ export interface ActionPayloads {
|
||||
loadPeerStoriesByIds: {
|
||||
peerId: string;
|
||||
storyIds: number[];
|
||||
} & WithTabId;
|
||||
};
|
||||
viewStory: {
|
||||
peerId: string;
|
||||
storyId: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user