Statistics: Story Stats for Channels (#4097)
This commit is contained in:
parent
74fd74203e
commit
78dac9bbaf
@ -204,8 +204,8 @@ export function buildApiMessageWithChatId(
|
|||||||
content,
|
content,
|
||||||
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,
|
viewsCount: mtpMessage.views,
|
||||||
forwards: mtpMessage.forwards,
|
forwardsCount: mtpMessage.forwards,
|
||||||
isScheduled,
|
isScheduled,
|
||||||
isFromScheduled: mtpMessage.fromScheduled,
|
isFromScheduled: mtpMessage.fromScheduled,
|
||||||
isSilent: mtpMessage.silent,
|
isSilent: mtpMessage.silent,
|
||||||
|
|||||||
@ -4,16 +4,19 @@ import type {
|
|||||||
ApiChannelStatistics,
|
ApiChannelStatistics,
|
||||||
ApiGroupStatistics,
|
ApiGroupStatistics,
|
||||||
ApiMessagePublicForward,
|
ApiMessagePublicForward,
|
||||||
ApiMessageStatistics,
|
ApiPostStatistics,
|
||||||
|
ApiStoryPublicForward,
|
||||||
StatisticsGraph,
|
StatisticsGraph,
|
||||||
StatisticsMessageInteractionCounter,
|
StatisticsMessageInteractionCounter,
|
||||||
StatisticsOverviewItem,
|
StatisticsOverviewItem,
|
||||||
StatisticsOverviewPercentage,
|
StatisticsOverviewPercentage,
|
||||||
StatisticsOverviewPeriod,
|
StatisticsOverviewPeriod,
|
||||||
|
StatisticsStoryInteractionCounter,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import { buildAvatarHash } from './chats';
|
import { buildAvatarHash } from './chats';
|
||||||
import { buildApiPeerId } from './peers';
|
import { buildApiUsernames } from './common';
|
||||||
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||||
|
|
||||||
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics {
|
export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiChannelStatistics {
|
||||||
return {
|
return {
|
||||||
@ -28,26 +31,43 @@ export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiC
|
|||||||
viewsBySourceGraph: (stats.viewsBySourceGraph as GramJs.StatsGraphAsync).token,
|
viewsBySourceGraph: (stats.viewsBySourceGraph as GramJs.StatsGraphAsync).token,
|
||||||
newFollowersBySourceGraph: (stats.newFollowersBySourceGraph as GramJs.StatsGraphAsync).token,
|
newFollowersBySourceGraph: (stats.newFollowersBySourceGraph as GramJs.StatsGraphAsync).token,
|
||||||
interactionsGraph: (stats.interactionsGraph 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,
|
||||||
|
|
||||||
// Statistics overview
|
// Statistics overview
|
||||||
followers: buildStatisticsOverview(stats.followers),
|
followers: buildStatisticsOverview(stats.followers),
|
||||||
viewsPerPost: buildStatisticsOverview(stats.viewsPerPost),
|
viewsPerPost: buildStatisticsOverview(stats.viewsPerPost),
|
||||||
sharesPerPost: buildStatisticsOverview(stats.sharesPerPost),
|
sharesPerPost: buildStatisticsOverview(stats.sharesPerPost),
|
||||||
enabledNotifications: buildStatisticsPercentage(stats.enabledNotifications),
|
enabledNotifications: buildStatisticsPercentage(stats.enabledNotifications),
|
||||||
|
reactionsPerPost: buildStatisticsOverview(stats.reactionsPerPost),
|
||||||
|
viewsPerStory: buildStatisticsOverview(stats.viewsPerStory),
|
||||||
|
sharesPerStory: buildStatisticsOverview(stats.sharesPerStory),
|
||||||
|
reactionsPerStory: buildStatisticsOverview(stats.reactionsPerStory),
|
||||||
|
|
||||||
// Recent posts
|
// Recent posts
|
||||||
recentTopMessages: stats.recentPostsInteractions.map(buildApiMessageInteractionCounter).filter(Boolean),
|
recentPosts: stats.recentPostsInteractions.map(buildApiPostInteractionCounter).filter(Boolean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildApiMessageInteractionCounter(
|
export function buildApiPostInteractionCounter(
|
||||||
interaction: GramJs.TypePostInteractionCounters,
|
interaction: GramJs.TypePostInteractionCounters,
|
||||||
): StatisticsMessageInteractionCounter | undefined {
|
): StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter | undefined {
|
||||||
if (interaction instanceof GramJs.PostInteractionCountersMessage) {
|
if (interaction instanceof GramJs.PostInteractionCountersMessage) {
|
||||||
return {
|
return {
|
||||||
msgId: interaction.msgId,
|
msgId: interaction.msgId,
|
||||||
forwards: interaction.forwards,
|
forwardsCount: interaction.forwards,
|
||||||
views: interaction.views,
|
viewsCount: interaction.views,
|
||||||
|
reactionsCount: interaction.reactions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction instanceof GramJs.PostInteractionCountersStory) {
|
||||||
|
return {
|
||||||
|
storyId: interaction.storyId,
|
||||||
|
reactionsCount: interaction.reactions,
|
||||||
|
viewsCount: interaction.views,
|
||||||
|
forwardsCount: interaction.forwards,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,9 +95,10 @@ export function buildGroupStatistics(stats: GramJs.stats.MegagroupStats): ApiGro
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMessageStatistics(stats: GramJs.stats.MessageStats): ApiMessageStatistics {
|
export function buildPostsStatistics(stats: GramJs.stats.MessageStats): ApiPostStatistics {
|
||||||
return {
|
return {
|
||||||
viewsGraph: buildGraph(stats.viewsGraph),
|
viewsGraph: buildGraph(stats.viewsGraph),
|
||||||
|
reactionsGraph: buildGraph(stats.reactionsByEmotionGraph),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,22 +109,30 @@ export function buildMessagePublicForwards(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.messages.map((message) => {
|
return result.messages.map((message) => buildApiMessagePublicForward(message, result.chats));
|
||||||
const peerId = buildApiPeerId((message.peerId as GramJs.PeerChannel).channelId, 'channel');
|
}
|
||||||
const channel = result.chats.find((p) => buildApiPeerId(p.id, 'channel') === peerId);
|
|
||||||
|
export function buildStoryPublicForwards(
|
||||||
|
result: GramJs.stats.PublicForwards,
|
||||||
|
): Array<ApiStoryPublicForward | ApiMessagePublicForward> | undefined {
|
||||||
|
if (!result || !('forwards' in result)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.forwards.map((forward) => {
|
||||||
|
if (forward instanceof GramJs.PublicForwardMessage) {
|
||||||
|
return buildApiMessagePublicForward(forward.message, result.chats);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { peer, story } = forward;
|
||||||
|
const peerId = getApiChatIdFromMtpPeer(peer);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messageId: message.id,
|
peerId,
|
||||||
views: (message as GramJs.Message).views,
|
storyId: story.id,
|
||||||
title: (channel as GramJs.Channel).title,
|
viewsCount: (story as GramJs.StoryItem).views?.viewsCount || 0,
|
||||||
chat: {
|
reactionsCount: (story as GramJs.StoryItem).views?.reactionsCount || 0,
|
||||||
id: peerId,
|
} as ApiStoryPublicForward;
|
||||||
type: 'chatTypeChannel',
|
|
||||||
title: (channel as GramJs.Channel).title,
|
|
||||||
username: (channel as GramJs.Channel).username,
|
|
||||||
avatarHash: buildAvatarHash((channel as GramJs.Channel).photo),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,3 +220,23 @@ function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewP
|
|||||||
minDate: data.minDate,
|
minDate: data.minDate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildApiMessagePublicForward(message: GramJs.TypeMessage, chats: GramJs.TypeChat[]): ApiMessagePublicForward {
|
||||||
|
const peerId = getApiChatIdFromMtpPeer(message.peerId!);
|
||||||
|
const channel = chats.find((c) => buildApiPeerId(c.id, 'channel') === peerId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId: message.id,
|
||||||
|
views: (message as GramJs.Message).views,
|
||||||
|
title: (channel as GramJs.Channel).title,
|
||||||
|
chat: {
|
||||||
|
id: peerId,
|
||||||
|
type: 'chatTypeChannel',
|
||||||
|
title: (channel as GramJs.Channel).title,
|
||||||
|
usernames: buildApiUsernames(channel as GramJs.Channel),
|
||||||
|
avatarHash: channel && 'photo' in channel
|
||||||
|
? buildAvatarHash((channel as GramJs.Channel).photo)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type {
|
|||||||
ApiMediaAreaCoordinates,
|
ApiMediaAreaCoordinates,
|
||||||
ApiStealthMode,
|
ApiStealthMode,
|
||||||
ApiStoryForwardInfo,
|
ApiStoryForwardInfo,
|
||||||
ApiStoryView,
|
ApiStoryView, ApiStoryViews,
|
||||||
ApiTypeStory,
|
ApiTypeStory,
|
||||||
MediaContent,
|
MediaContent,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
@ -67,13 +67,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
|||||||
...(selectedContacts && { isForSelectedContacts: true }),
|
...(selectedContacts && { isForSelectedContacts: true }),
|
||||||
...(closeFriends && { isForCloseFriends: true }),
|
...(closeFriends && { isForCloseFriends: true }),
|
||||||
...(noforwards && { noForwards: true }),
|
...(noforwards && { noForwards: true }),
|
||||||
...(views?.viewsCount && { viewsCount: views.viewsCount }),
|
...(views && { views: buildApiStoryViews(views) }),
|
||||||
...(views?.forwardsCount && { forwardsCount: views.forwardsCount }),
|
|
||||||
...(views?.reactionsCount && { reactionsCount: views.reactionsCount }),
|
|
||||||
...(views?.reactions && { reactions: views.reactions.map(buildReactionCount) }),
|
|
||||||
...(views?.recentViewers && {
|
|
||||||
recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')),
|
|
||||||
}),
|
|
||||||
...(out && { isOut: true }),
|
...(out && { isOut: true }),
|
||||||
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
||||||
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
|
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
|
||||||
@ -82,6 +76,18 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildApiStoryViews(views: GramJs.TypeStoryViews): ApiStoryViews | undefined {
|
||||||
|
return {
|
||||||
|
viewsCount: views.viewsCount,
|
||||||
|
forwardsCount: views.forwardsCount,
|
||||||
|
reactionsCount: views.reactionsCount,
|
||||||
|
...(views?.reactions && { reactions: views.reactions.map(buildReactionCount).filter(Boolean) }),
|
||||||
|
...(views?.recentViewers && {
|
||||||
|
recentViewerIds: views.recentViewers.map((viewerId) => buildApiPeerId(viewerId, 'user')),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildApiStoryView(view: GramJs.TypeStoryView): ApiStoryView {
|
export function buildApiStoryView(view: GramJs.TypeStoryView): ApiStoryView {
|
||||||
const {
|
const {
|
||||||
userId, date, reaction, blockedMyStoriesFrom, blocked,
|
userId, date, reaction, blockedMyStoriesFrom, blocked,
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics,
|
fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics,
|
||||||
fetchMessagePublicForwards, fetchStatisticsAsyncGraph,
|
fetchMessagePublicForwards, fetchStatisticsAsyncGraph, fetchStoryStatistics, fetchStoryPublicForwards,
|
||||||
} from './statistics';
|
} from './statistics';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -2,15 +2,21 @@ import BigInt from 'big-integer';
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiMessagePublicForward, ApiMessageStatistics, StatisticsGraph,
|
ApiChat, ApiMessagePublicForward, ApiPostStatistics, ApiStoryPublicForward, ApiUser, StatisticsGraph,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
|
import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config';
|
||||||
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import {
|
import {
|
||||||
buildChannelStatistics, buildGraph,
|
buildChannelStatistics,
|
||||||
buildGroupStatistics, buildMessagePublicForwards, buildMessageStatistics,
|
buildGraph,
|
||||||
|
buildGroupStatistics,
|
||||||
|
buildMessagePublicForwards,
|
||||||
|
buildPostsStatistics,
|
||||||
|
buildStoryPublicForwards,
|
||||||
} from '../apiBuilders/statistics';
|
} from '../apiBuilders/statistics';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import { buildInputEntity } from '../gramjsBuilders';
|
import { buildInputEntity, buildInputPeer } from '../gramjsBuilders';
|
||||||
import { addEntitiesToLocalDb } from '../helpers';
|
import { addEntitiesToLocalDb } from '../helpers';
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
|
|
||||||
@ -62,7 +68,7 @@ export async function fetchMessageStatistics({
|
|||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
dcId?: number;
|
dcId?: number;
|
||||||
}): Promise<ApiMessageStatistics | undefined> {
|
}): Promise<ApiPostStatistics | undefined> {
|
||||||
const result = await invokeRequest(new GramJs.stats.GetMessageStats({
|
const result = await invokeRequest(new GramJs.stats.GetMessageStats({
|
||||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||||
msgId: messageId,
|
msgId: messageId,
|
||||||
@ -74,22 +80,30 @@ export async function fetchMessageStatistics({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildMessageStatistics(result);
|
return buildPostsStatistics(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMessagePublicForwards({
|
export async function fetchMessagePublicForwards({
|
||||||
chat,
|
chat,
|
||||||
messageId,
|
messageId,
|
||||||
dcId,
|
dcId,
|
||||||
|
offsetRate = 0,
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
dcId?: number;
|
dcId?: number;
|
||||||
}): Promise<ApiMessagePublicForward[] | undefined> {
|
offsetRate?: number;
|
||||||
|
}): Promise<{
|
||||||
|
forwards?: ApiMessagePublicForward[];
|
||||||
|
count?: number;
|
||||||
|
nextRate?: number;
|
||||||
|
} | undefined> {
|
||||||
const result = await invokeRequest(new GramJs.stats.GetMessagePublicForwards({
|
const result = await invokeRequest(new GramJs.stats.GetMessagePublicForwards({
|
||||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||||
msgId: messageId,
|
msgId: messageId,
|
||||||
offsetPeer: new GramJs.InputPeerEmpty(),
|
offsetPeer: new GramJs.InputPeerEmpty(),
|
||||||
|
offsetRate,
|
||||||
|
limit: STATISTICS_PUBLIC_FORWARDS_LIMIT,
|
||||||
}), {
|
}), {
|
||||||
dcId,
|
dcId,
|
||||||
});
|
});
|
||||||
@ -102,7 +116,13 @@ export async function fetchMessagePublicForwards({
|
|||||||
addEntitiesToLocalDb(result.chats);
|
addEntitiesToLocalDb(result.chats);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildMessagePublicForwards(result);
|
return {
|
||||||
|
forwards: buildMessagePublicForwards(result),
|
||||||
|
...('nextRate' in result ? {
|
||||||
|
count: result.count,
|
||||||
|
nextRate: result.nextRate,
|
||||||
|
} : undefined),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStatisticsAsyncGraph({
|
export async function fetchStatisticsAsyncGraph({
|
||||||
@ -129,3 +149,68 @@ export async function fetchStatisticsAsyncGraph({
|
|||||||
|
|
||||||
return buildGraph(result as GramJs.StatsGraph, isPercentage);
|
return buildGraph(result as GramJs.StatsGraph, isPercentage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchStoryStatistics({
|
||||||
|
chat,
|
||||||
|
storyId,
|
||||||
|
dcId,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
storyId: number;
|
||||||
|
dcId?: number;
|
||||||
|
}): Promise<ApiPostStatistics | undefined> {
|
||||||
|
const result = await invokeRequest(new GramJs.stats.GetStoryStats({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
id: storyId,
|
||||||
|
}), {
|
||||||
|
dcId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildPostsStatistics(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchStoryPublicForwards({
|
||||||
|
chat,
|
||||||
|
storyId,
|
||||||
|
dcId,
|
||||||
|
offsetId = '0',
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
storyId: number;
|
||||||
|
dcId?: number;
|
||||||
|
offsetId?: string;
|
||||||
|
}): Promise<{
|
||||||
|
publicForwards: (ApiMessagePublicForward | ApiStoryPublicForward)[] | undefined;
|
||||||
|
users: ApiUser[];
|
||||||
|
chats: ApiChat[];
|
||||||
|
count?: number;
|
||||||
|
nextOffsetId?: string;
|
||||||
|
} | undefined> {
|
||||||
|
const result = await invokeRequest(new GramJs.stats.GetStoryPublicForwards({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
id: storyId,
|
||||||
|
offset: offsetId,
|
||||||
|
limit: STATISTICS_PUBLIC_FORWARDS_LIMIT,
|
||||||
|
}), {
|
||||||
|
dcId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEntitiesToLocalDb(result.chats);
|
||||||
|
addEntitiesToLocalDb(result.users);
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicForwards: buildStoryPublicForwards(result),
|
||||||
|
users: result.users.map(buildApiUser).filter(Boolean),
|
||||||
|
chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
|
||||||
|
count: result.count,
|
||||||
|
nextOffsetId: result.nextOffset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -522,7 +522,7 @@ export function updater(update: Update) {
|
|||||||
'@type': 'updateMessage',
|
'@type': 'updateMessage',
|
||||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||||
id: update.id,
|
id: update.id,
|
||||||
message: { views: update.views },
|
message: { viewsCount: update.views },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chats
|
// Chats
|
||||||
|
|||||||
@ -472,8 +472,8 @@ export interface ApiMessage {
|
|||||||
forwardInfo?: ApiMessageForwardInfo;
|
forwardInfo?: ApiMessageForwardInfo;
|
||||||
isDeleting?: boolean;
|
isDeleting?: boolean;
|
||||||
previousLocalId?: number;
|
previousLocalId?: number;
|
||||||
views?: number;
|
viewsCount?: number;
|
||||||
forwards?: number;
|
forwardsCount?: number;
|
||||||
isEdited?: boolean;
|
isEdited?: boolean;
|
||||||
editDate?: number;
|
editDate?: number;
|
||||||
isMentioned?: boolean;
|
isMentioned?: boolean;
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import type { ApiChat } from './chats';
|
import type { ApiChat } from './chats';
|
||||||
import type { ApiMessage } from './messages';
|
|
||||||
|
|
||||||
export interface ApiChannelStatistics {
|
export interface ApiChannelStatistics {
|
||||||
growthGraph?: StatisticsGraph | string;
|
growthGraph?: StatisticsGraph | string;
|
||||||
followersGraph?: StatisticsGraph | string;
|
followersGraph?: StatisticsGraph | string;
|
||||||
muteGraph?: StatisticsGraph | string;
|
muteGraph?: StatisticsGraph | string;
|
||||||
topHoursGraph?: StatisticsGraph | string;
|
topHoursGraph?: StatisticsGraph | string;
|
||||||
|
reactionsByEmotionGraph?: StatisticsGraph | string;
|
||||||
|
storyInteractionsGraph?: StatisticsGraph | string;
|
||||||
|
storyReactionsByEmotionGraph?: StatisticsGraph | string;
|
||||||
interactionsGraph: StatisticsGraph | string;
|
interactionsGraph: StatisticsGraph | string;
|
||||||
viewsBySourceGraph: StatisticsGraph | string;
|
viewsBySourceGraph: StatisticsGraph | string;
|
||||||
newFollowersBySourceGraph: StatisticsGraph | string;
|
newFollowersBySourceGraph: StatisticsGraph | string;
|
||||||
@ -14,7 +16,11 @@ export interface ApiChannelStatistics {
|
|||||||
viewsPerPost: StatisticsOverviewItem;
|
viewsPerPost: StatisticsOverviewItem;
|
||||||
sharesPerPost: StatisticsOverviewItem;
|
sharesPerPost: StatisticsOverviewItem;
|
||||||
enabledNotifications: StatisticsOverviewPercentage;
|
enabledNotifications: StatisticsOverviewPercentage;
|
||||||
recentTopMessages: Array<StatisticsMessageInteractionCounter | StatisticsMessageInteractionCounter & ApiMessage>;
|
reactionsPerPost: StatisticsOverviewItem;
|
||||||
|
viewsPerStory: StatisticsOverviewItem;
|
||||||
|
sharesPerStory: StatisticsOverviewItem;
|
||||||
|
reactionsPerStory: StatisticsOverviewItem;
|
||||||
|
recentPosts: Array<StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiGroupStatistics {
|
export interface ApiGroupStatistics {
|
||||||
@ -31,12 +37,17 @@ export interface ApiGroupStatistics {
|
|||||||
posters: StatisticsOverviewItem;
|
posters: StatisticsOverviewItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiMessageStatistics {
|
export interface ApiPostStatistics {
|
||||||
viewsGraph?: StatisticsGraph | string;
|
viewsGraph?: StatisticsGraph | string;
|
||||||
forwards?: number;
|
reactionsGraph?: StatisticsGraph | string;
|
||||||
views?: number;
|
forwardsCount?: number;
|
||||||
|
viewsCount?: number;
|
||||||
|
reactionsCount?: number;
|
||||||
publicForwards?: number;
|
publicForwards?: number;
|
||||||
publicForwardsData?: ApiMessagePublicForward[];
|
publicForwardsData?: (ApiMessagePublicForward | ApiStoryPublicForward)[];
|
||||||
|
|
||||||
|
nextRate?: number;
|
||||||
|
nextOffsetId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiBoostStatistics {
|
export interface ApiBoostStatistics {
|
||||||
@ -53,6 +64,13 @@ export interface ApiMessagePublicForward {
|
|||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiStoryPublicForward {
|
||||||
|
peerId: string;
|
||||||
|
storyId: number;
|
||||||
|
viewsCount?: number;
|
||||||
|
reactionsCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StatisticsGraph {
|
export interface StatisticsGraph {
|
||||||
type: string;
|
type: string;
|
||||||
zoomToken?: string;
|
zoomToken?: string;
|
||||||
@ -95,6 +113,14 @@ export interface StatisticsOverviewPeriod {
|
|||||||
|
|
||||||
export interface StatisticsMessageInteractionCounter {
|
export interface StatisticsMessageInteractionCounter {
|
||||||
msgId: number;
|
msgId: number;
|
||||||
forwards: number;
|
forwardsCount: number;
|
||||||
views: number;
|
viewsCount: number;
|
||||||
|
reactionsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatisticsStoryInteractionCounter {
|
||||||
|
storyId: number;
|
||||||
|
viewsCount: number;
|
||||||
|
forwardsCount: number;
|
||||||
|
reactionsCount: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,15 +18,19 @@ export interface ApiStory {
|
|||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
isOut?: true;
|
isOut?: true;
|
||||||
noForwards?: boolean;
|
noForwards?: boolean;
|
||||||
|
views?: ApiStoryViews;
|
||||||
|
visibility?: ApiPrivacySettings;
|
||||||
|
sentReaction?: ApiReaction;
|
||||||
|
mediaAreas?: ApiMediaArea[];
|
||||||
|
forwardInfo?: ApiStoryForwardInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiStoryViews {
|
||||||
viewsCount?: number;
|
viewsCount?: number;
|
||||||
forwardsCount?: number;
|
forwardsCount?: number;
|
||||||
reactionsCount?: number;
|
reactionsCount?: number;
|
||||||
reactions?: ApiReactionCount[];
|
reactions?: ApiReactionCount[];
|
||||||
recentViewerIds?: string[];
|
recentViewerIds?: string[];
|
||||||
visibility?: ApiPrivacySettings;
|
|
||||||
sentReaction?: ApiReaction;
|
|
||||||
mediaAreas?: ApiMediaArea[];
|
|
||||||
forwardInfo?: ApiStoryForwardInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiStorySkipped {
|
export interface ApiStorySkipped {
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export { default as StickerSearch } from '../components/right/StickerSearch';
|
|||||||
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 MessageStatistics } from '../components/right/statistics/MessageStatistics';
|
||||||
|
export { default as StoryStatistics } from '../components/right/statistics/StoryStatistics';
|
||||||
export { default as PollResults } from '../components/right/PollResults';
|
export { default as PollResults } from '../components/right/PollResults';
|
||||||
export { default as CreateTopic } from '../components/right/CreateTopic';
|
export { default as CreateTopic } from '../components/right/CreateTopic';
|
||||||
export { default as EditTopic } from '../components/right/EditTopic';
|
export { default as EditTopic } from '../components/right/EditTopic';
|
||||||
|
|||||||
@ -214,6 +214,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.size-small-mobile {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.online::after {
|
&.online::after {
|
||||||
bottom: -0.125rem;
|
bottom: -0.125rem;
|
||||||
right: -0.125rem;
|
right: -0.125rem;
|
||||||
|
|||||||
@ -56,6 +56,7 @@ type OwnProps = {
|
|||||||
forPremiumPromo?: boolean;
|
forPremiumPromo?: boolean;
|
||||||
withStoryGap?: boolean;
|
withStoryGap?: boolean;
|
||||||
withStorySolid?: boolean;
|
withStorySolid?: boolean;
|
||||||
|
forceUnreadStorySolid?: boolean;
|
||||||
storyViewerOrigin?: StoryViewerOrigin;
|
storyViewerOrigin?: StoryViewerOrigin;
|
||||||
storyViewerMode?: 'full' | 'single-peer' | 'disabled';
|
storyViewerMode?: 'full' | 'single-peer' | 'disabled';
|
||||||
loopIndefinitely?: boolean;
|
loopIndefinitely?: boolean;
|
||||||
@ -76,6 +77,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
forPremiumPromo,
|
forPremiumPromo,
|
||||||
withStoryGap,
|
withStoryGap,
|
||||||
withStorySolid,
|
withStorySolid,
|
||||||
|
forceUnreadStorySolid,
|
||||||
storyViewerOrigin,
|
storyViewerOrigin,
|
||||||
storyViewerMode = 'single-peer',
|
storyViewerMode = 'single-peer',
|
||||||
loopIndefinitely,
|
loopIndefinitely,
|
||||||
@ -217,7 +219,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
isForum && 'forum',
|
isForum && 'forum',
|
||||||
((withStory && peer?.hasStories) || forPremiumPromo) && 'with-story-circle',
|
((withStory && peer?.hasStories) || forPremiumPromo) && 'with-story-circle',
|
||||||
withStorySolid && peer?.hasStories && 'with-story-solid',
|
withStorySolid && peer?.hasStories && 'with-story-solid',
|
||||||
withStorySolid && peer?.hasUnreadStories && 'has-unread-story',
|
withStorySolid && (peer?.hasUnreadStories || forceUnreadStorySolid) && 'has-unread-story',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -295,7 +295,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
const ids = messageIds.filter((id) => selectThreadInfo(global, chatId, id)?.isCommentsInfo
|
const ids = messageIds.filter((id) => selectThreadInfo(global, chatId, id)?.isCommentsInfo
|
||||||
|| messagesById[id]?.views !== undefined);
|
|| messagesById[id]?.viewsCount !== undefined);
|
||||||
|
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
|
|
||||||
|
|||||||
@ -613,7 +613,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isLastInDocumentGroup && 'last-in-document-group',
|
isLastInDocumentGroup && 'last-in-document-group',
|
||||||
isLastInList && 'last-in-list',
|
isLastInList && 'last-in-list',
|
||||||
isOwn && 'own',
|
isOwn && 'own',
|
||||||
Boolean(message.views) && 'has-views',
|
Boolean(message.viewsCount) && 'has-views',
|
||||||
message.isEdited && 'was-edited',
|
message.isEdited && 'was-edited',
|
||||||
hasMessageReply && 'has-reply',
|
hasMessageReply && 'has-reply',
|
||||||
isContextMenuOpen && 'has-menu-open',
|
isContextMenuOpen && 'has-menu-open',
|
||||||
@ -1338,7 +1338,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
data-has-unread-mention={message.hasUnreadMention || undefined}
|
data-has-unread-mention={message.hasUnreadMention || undefined}
|
||||||
data-has-unread-reaction={hasUnreadReaction || undefined}
|
data-has-unread-reaction={hasUnreadReaction || undefined}
|
||||||
data-is-pinned={isPinned || undefined}
|
data-is-pinned={isPinned || undefined}
|
||||||
data-should-update-views={message.views !== undefined}
|
data-should-update-views={message.viewsCount !== undefined}
|
||||||
/>
|
/>
|
||||||
{!isInDocumentGroup && (
|
{!isInDocumentGroup && (
|
||||||
<div className="message-select-control">
|
<div className="message-select-control">
|
||||||
|
|||||||
@ -105,10 +105,10 @@ const MessageMeta: FC<OwnProps> = ({
|
|||||||
{isTranslated && (
|
{isTranslated && (
|
||||||
<i className="icon icon-language message-translated" onClick={onTranslationClick} />
|
<i className="icon icon-language message-translated" onClick={onTranslationClick} />
|
||||||
)}
|
)}
|
||||||
{Boolean(message.views) && (
|
{Boolean(message.viewsCount) && (
|
||||||
<>
|
<>
|
||||||
<span className="message-views">
|
<span className="message-views">
|
||||||
{formatIntegerCompact(message.views!)}
|
{formatIntegerCompact(message.viewsCount!)}
|
||||||
</span>
|
</span>
|
||||||
<i className="icon icon-channelviews" />
|
<i className="icon icon-channelviews" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import RightSearch from './RightSearch.async';
|
|||||||
import BoostStatistics from './statistics/BoostStatistics';
|
import BoostStatistics from './statistics/BoostStatistics';
|
||||||
import MessageStatistics from './statistics/MessageStatistics.async';
|
import MessageStatistics from './statistics/MessageStatistics.async';
|
||||||
import Statistics from './statistics/Statistics.async';
|
import Statistics from './statistics/Statistics.async';
|
||||||
|
import StoryStatistics from './statistics/StoryStatistics.async';
|
||||||
import StickerSearch from './StickerSearch.async';
|
import StickerSearch from './StickerSearch.async';
|
||||||
|
|
||||||
import './RightColumn.scss';
|
import './RightColumn.scss';
|
||||||
@ -86,6 +87,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
setEditingExportedInvite,
|
setEditingExportedInvite,
|
||||||
toggleStatistics,
|
toggleStatistics,
|
||||||
toggleMessageStatistics,
|
toggleMessageStatistics,
|
||||||
|
toggleStoryStatistics,
|
||||||
setOpenedInviteInfo,
|
setOpenedInviteInfo,
|
||||||
requestNextManagementScreen,
|
requestNextManagementScreen,
|
||||||
resetNextProfileTab,
|
resetNextProfileTab,
|
||||||
@ -107,6 +109,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
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 isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
|
||||||
|
const isStoryStatistics = contentKey === RightColumnContent.StoryStatistics;
|
||||||
const isBoostStatistics = contentKey === RightColumnContent.BoostStatistics;
|
const isBoostStatistics = contentKey === RightColumnContent.BoostStatistics;
|
||||||
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
||||||
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
||||||
@ -176,6 +179,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
case RightColumnContent.MessageStatistics:
|
case RightColumnContent.MessageStatistics:
|
||||||
toggleMessageStatistics();
|
toggleMessageStatistics();
|
||||||
break;
|
break;
|
||||||
|
case RightColumnContent.StoryStatistics:
|
||||||
|
toggleStoryStatistics();
|
||||||
|
break;
|
||||||
case RightColumnContent.Statistics:
|
case RightColumnContent.Statistics:
|
||||||
toggleStatistics();
|
toggleStatistics();
|
||||||
break;
|
break;
|
||||||
@ -322,6 +328,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
return <BoostStatistics />;
|
return <BoostStatistics />;
|
||||||
case RightColumnContent.MessageStatistics:
|
case RightColumnContent.MessageStatistics:
|
||||||
return <MessageStatistics chatId={chatId!} isActive={isOpen && isActive} />;
|
return <MessageStatistics chatId={chatId!} isActive={isOpen && isActive} />;
|
||||||
|
case RightColumnContent.StoryStatistics:
|
||||||
|
return <StoryStatistics 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:
|
||||||
@ -356,6 +364,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
isStatistics={isStatistics}
|
isStatistics={isStatistics}
|
||||||
isBoostStatistics={isBoostStatistics}
|
isBoostStatistics={isBoostStatistics}
|
||||||
isMessageStatistics={isMessageStatistics}
|
isMessageStatistics={isMessageStatistics}
|
||||||
|
isStoryStatistics={isStoryStatistics}
|
||||||
isStickerSearch={isStickerSearch}
|
isStickerSearch={isStickerSearch}
|
||||||
isGifSearch={isGifSearch}
|
isGifSearch={isGifSearch}
|
||||||
isPollResults={isPollResults}
|
isPollResults={isPollResults}
|
||||||
@ -373,7 +382,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
|
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
|
||||||
shouldCleanup
|
shouldCleanup
|
||||||
cleanupExceptionKey={
|
cleanupExceptionKey={
|
||||||
renderingContentKey === RightColumnContent.MessageStatistics
|
(renderingContentKey === RightColumnContent.MessageStatistics
|
||||||
|
|| renderingContentKey === RightColumnContent.StoryStatistics)
|
||||||
? RightColumnContent.Statistics : undefined
|
? RightColumnContent.Statistics : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -48,6 +48,7 @@ type OwnProps = {
|
|||||||
isStatistics?: boolean;
|
isStatistics?: boolean;
|
||||||
isBoostStatistics?: boolean;
|
isBoostStatistics?: boolean;
|
||||||
isMessageStatistics?: boolean;
|
isMessageStatistics?: boolean;
|
||||||
|
isStoryStatistics?: boolean;
|
||||||
isStickerSearch?: boolean;
|
isStickerSearch?: boolean;
|
||||||
isGifSearch?: boolean;
|
isGifSearch?: boolean;
|
||||||
isPollResults?: boolean;
|
isPollResults?: boolean;
|
||||||
@ -89,6 +90,7 @@ enum HeaderContent {
|
|||||||
Search,
|
Search,
|
||||||
Statistics,
|
Statistics,
|
||||||
MessageStatistics,
|
MessageStatistics,
|
||||||
|
StoryStatistics,
|
||||||
BoostStatistics,
|
BoostStatistics,
|
||||||
Management,
|
Management,
|
||||||
ManageInitial,
|
ManageInitial,
|
||||||
@ -128,6 +130,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
isManagement,
|
isManagement,
|
||||||
isStatistics,
|
isStatistics,
|
||||||
isMessageStatistics,
|
isMessageStatistics,
|
||||||
|
isStoryStatistics,
|
||||||
isBoostStatistics,
|
isBoostStatistics,
|
||||||
isStickerSearch,
|
isStickerSearch,
|
||||||
isGifSearch,
|
isGifSearch,
|
||||||
@ -291,6 +294,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
HeaderContent.Statistics
|
HeaderContent.Statistics
|
||||||
) : isMessageStatistics ? (
|
) : isMessageStatistics ? (
|
||||||
HeaderContent.MessageStatistics
|
HeaderContent.MessageStatistics
|
||||||
|
) : isStoryStatistics ? (
|
||||||
|
HeaderContent.StoryStatistics
|
||||||
) : isBoostStatistics ? (
|
) : isBoostStatistics ? (
|
||||||
HeaderContent.BoostStatistics
|
HeaderContent.BoostStatistics
|
||||||
) : isCreatingTopic ? (
|
) : isCreatingTopic ? (
|
||||||
@ -442,6 +447,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
|
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
|
||||||
case HeaderContent.MessageStatistics:
|
case HeaderContent.MessageStatistics:
|
||||||
return <h3>{lang('Stats.MessageTitle')}</h3>;
|
return <h3>{lang('Stats.MessageTitle')}</h3>;
|
||||||
|
case HeaderContent.StoryStatistics:
|
||||||
|
return <h3>{lang('Stats.StoryTitle')}</h3>;
|
||||||
case HeaderContent.BoostStatistics:
|
case HeaderContent.BoostStatistics:
|
||||||
return <h3>{lang('Boosts')}</h3>;
|
return <h3>{lang('Boosts')}</h3>;
|
||||||
case HeaderContent.SharedMedia:
|
case HeaderContent.SharedMedia:
|
||||||
@ -522,6 +529,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
|| contentKey === HeaderContent.StoryList
|
|| contentKey === HeaderContent.StoryList
|
||||||
|| contentKey === HeaderContent.AddingMembers
|
|| contentKey === HeaderContent.AddingMembers
|
||||||
|| contentKey === HeaderContent.MessageStatistics
|
|| contentKey === HeaderContent.MessageStatistics
|
||||||
|
|| contentKey === HeaderContent.StoryStatistics
|
||||||
|| isManagement
|
|| isManagement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,30 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useEffect, useRef,
|
memo, useEffect, useRef, useState,
|
||||||
useState,
|
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiMessagePublicForward, ApiMessageStatistics, StatisticsGraph } from '../../../api/types';
|
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 { selectChatFullInfo, selectTabState } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
|
|
||||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
|
import StatisticsMessagePublicForward from './StatisticsMessagePublicForward';
|
||||||
import StatisticsOverview from './StatisticsOverview';
|
import StatisticsOverview from './StatisticsOverview';
|
||||||
import StatisticsPublicForward from './StatisticsPublicForward';
|
|
||||||
|
|
||||||
import './Statistics.scss';
|
import styles from './Statistics.module.scss';
|
||||||
|
|
||||||
type ILovelyChart = { create: Function };
|
type ILovelyChart = { create: Function };
|
||||||
let lovelyChartPromise: Promise<ILovelyChart>;
|
let lovelyChartPromise: Promise<ILovelyChart>;
|
||||||
@ -35,8 +41,9 @@ async function ensureLovelyChart() {
|
|||||||
|
|
||||||
const GRAPH_TITLES = {
|
const GRAPH_TITLES = {
|
||||||
viewsGraph: 'Stats.MessageInteractionsTitle',
|
viewsGraph: 'Stats.MessageInteractionsTitle',
|
||||||
|
reactionsGraph: 'ReactionsByEmotionChartTitle',
|
||||||
};
|
};
|
||||||
const GRAPHS = Object.keys(GRAPH_TITLES) as (keyof ApiMessageStatistics)[];
|
const GRAPHS = Object.keys(GRAPH_TITLES) as (keyof ApiPostStatistics)[];
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -44,25 +51,25 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
statistics?: ApiMessageStatistics;
|
statistics?: ApiPostStatistics;
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
dcId?: number;
|
dcId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Statistics: FC<OwnProps & StateProps> = ({
|
function Statistics({
|
||||||
chatId,
|
chatId,
|
||||||
isActive,
|
isActive,
|
||||||
statistics,
|
statistics,
|
||||||
dcId,
|
dcId,
|
||||||
messageId,
|
messageId,
|
||||||
}) => {
|
}: OwnProps & StateProps) {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
const loadedCharts = useRef<string[]>([]);
|
const loadedCharts = useRef<string[]>([]);
|
||||||
|
|
||||||
const { loadMessageStatistics, loadStatisticsAsyncGraph } = getActions();
|
const { loadMessageStatistics, loadMessagePublicForwards, loadStatisticsAsyncGraph } = getActions();
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -144,34 +151,48 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
isReady, statistics, lang, chatId, messageId, loadStatisticsAsyncGraph, dcId, forceUpdate,
|
isReady, statistics, lang, chatId, messageId, loadStatisticsAsyncGraph, dcId, forceUpdate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const handleLoadMore = useLastCallback(({ direction }: { direction: LoadMoreDirection }) => {
|
||||||
|
if (direction === LoadMoreDirection.Backwards && messageId) {
|
||||||
|
loadMessagePublicForwards({ chatId, messageId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!isReady || !statistics || !messageId) {
|
if (!isReady || !statistics || !messageId) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
|
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
|
||||||
<StatisticsOverview statistics={statistics} type="message" title={lang('StatisticOverview')} />
|
<StatisticsOverview statistics={statistics} type="message" title={lang('StatisticOverview')} />
|
||||||
|
|
||||||
{!loadedCharts.current.length && <Loading />}
|
{!loadedCharts.current.length && <Loading />}
|
||||||
|
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
{GRAPHS.map((graph) => (
|
{GRAPHS.map((graph) => (
|
||||||
<div className={buildClassName('Statistics__graph', !loadedCharts.current.includes(graph) && 'hidden')} />
|
<div className={buildClassName(styles.graph, !loadedCharts.current.includes(graph) && styles.hidden)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Boolean(statistics.publicForwards) && (
|
{Boolean(statistics.publicForwards) && (
|
||||||
<div className="Statistics__public-forwards">
|
<div className={styles.publicForwards}>
|
||||||
<h2 className="Statistics__public-forwards-title">{lang('Stats.Message.PublicShares')}</h2>
|
<h2 className={styles.publicForwardsTitle}>{lang('Stats.Message.PublicShares')}</h2>
|
||||||
|
|
||||||
{statistics.publicForwardsData!.map((item: ApiMessagePublicForward) => (
|
<InfiniteScroll
|
||||||
<StatisticsPublicForward data={item} />
|
items={statistics.publicForwardsData}
|
||||||
))}
|
itemSelector=".statistic-public-forward"
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
preloadBackwards={STATISTICS_PUBLIC_FORWARDS_LIMIT}
|
||||||
|
noFastList
|
||||||
|
>
|
||||||
|
{(statistics.publicForwardsData as ApiMessagePublicForward[]).map((item) => (
|
||||||
|
<StatisticsMessagePublicForward key={item.messageId} data={item} />
|
||||||
|
))}
|
||||||
|
</InfiniteScroll>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId }): StateProps => {
|
(global, { chatId }): StateProps => {
|
||||||
|
|||||||
104
src/components/right/statistics/Statistics.module.scss
Normal file
104
src/components/right/statistics/Statistics.module.scss
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
.root {
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
:global(.lovely-chart--container) {
|
||||||
|
font: inherit !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lovely-chart--header) {
|
||||||
|
margin: 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lovely-chart--header),
|
||||||
|
:global(.lovely-chart--tooltip-title),
|
||||||
|
:global(.lovely-chart--tooltip-dataset-value),
|
||||||
|
:global(.lovely-chart--percentage-title) {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lovely-chart--container-type-pie) {
|
||||||
|
&:global(.lovely-chart--state-zoomed-in > canvas) {
|
||||||
|
animation-name: pie-slim-in !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(:not(.lovely-chart--state-zoomed-in) > canvas) {
|
||||||
|
animation-name: pie-slim-out !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages, .publicForwards {
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-top: 1px solid var(--color-borders);
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 30px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
|
||||||
|
&:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ready {
|
||||||
|
overflow-y: scroll !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pie-slim-in {
|
||||||
|
0% {
|
||||||
|
clip-path: circle(80% at center calc(50% - 7.5px));
|
||||||
|
transform: rotate(-360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
clip-path: circle(40% at center calc(50% - 7.5px));
|
||||||
|
transform: rotate(-360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
clip-path: circle(40% at center calc(50% - 7.5px));
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pie-slim-out {
|
||||||
|
0% {
|
||||||
|
clip-path: circle(40% at center calc(50% - 7.5px));
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
clip-path: circle(40% at center calc(50% - 7.5px));
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
clip-path: circle(80% at center calc(50% - 7.5px));
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,110 +0,0 @@
|
|||||||
.Statistics {
|
|
||||||
height: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
|
|
||||||
&__messages, &__public-forwards {
|
|
||||||
padding: 1rem 0;
|
|
||||||
border-top: 1px solid var(--color-borders);
|
|
||||||
|
|
||||||
&-title {
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--text-color);
|
|
||||||
line-height: 30px;
|
|
||||||
text-transform: lowercase;
|
|
||||||
|
|
||||||
&:first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ready {
|
|
||||||
overflow-y: scroll !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__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;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lovely-chart--container {
|
|
||||||
font: inherit !important;
|
|
||||||
font-size: 13px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lovely-chart--header {
|
|
||||||
margin: 0 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lovely-chart--header,
|
|
||||||
.lovely-chart--tooltip-title,
|
|
||||||
.lovely-chart--tooltip-dataset-value,
|
|
||||||
.lovely-chart--percentage-title {
|
|
||||||
font-weight: 500 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lovely-chart--container-type-pie {
|
|
||||||
&.lovely-chart--state-zoomed-in > canvas {
|
|
||||||
animation-name: pie-slim-in !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.lovely-chart--state-zoomed-in) > canvas {
|
|
||||||
animation-name: pie-slim-out !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pie-slim-in {
|
|
||||||
0% {
|
|
||||||
clip-path: circle(80% at center calc(50% - 7.5px));
|
|
||||||
-webkit-clip-path: circle(80% at center calc(50% - 7.5px));
|
|
||||||
transform: rotate(-360deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
25% {
|
|
||||||
clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
-webkit-clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
transform: rotate(-360deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
-webkit-clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pie-slim-out {
|
|
||||||
0% {
|
|
||||||
clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
-webkit-clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
-webkit-clip-path: circle(40% at center calc(50% - 7.5px));
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
75% {
|
|
||||||
clip-path: circle(80% at center calc(50% - 7.5px));
|
|
||||||
-webkit-clip-path: circle(80% at center calc(50% - 7.5px));
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,13 +7,20 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChannelStatistics,
|
ApiChannelStatistics,
|
||||||
|
ApiChat,
|
||||||
ApiGroupStatistics,
|
ApiGroupStatistics,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
|
ApiTypeStory,
|
||||||
StatisticsGraph,
|
StatisticsGraph,
|
||||||
StatisticsMessageInteractionCounter,
|
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
|
|
||||||
import { selectChat, selectChatFullInfo, selectStatistics } from '../../../global/selectors';
|
import {
|
||||||
|
selectChat,
|
||||||
|
selectChatFullInfo,
|
||||||
|
selectChatMessages,
|
||||||
|
selectPeerStories,
|
||||||
|
selectStatistics,
|
||||||
|
} from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
|
|
||||||
@ -23,8 +30,9 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import StatisticsOverview from './StatisticsOverview';
|
import StatisticsOverview from './StatisticsOverview';
|
||||||
import StatisticsRecentMessage from './StatisticsRecentMessage';
|
import StatisticsRecentMessage from './StatisticsRecentMessage';
|
||||||
|
import StatisticsRecentStory from './StatisticsRecentStory';
|
||||||
|
|
||||||
import './Statistics.scss';
|
import styles from './Statistics.module.scss';
|
||||||
|
|
||||||
type ILovelyChart = { create: Function };
|
type ILovelyChart = { create: Function };
|
||||||
let lovelyChartPromise: Promise<ILovelyChart>;
|
let lovelyChartPromise: Promise<ILovelyChart>;
|
||||||
@ -48,6 +56,9 @@ const CHANNEL_GRAPHS_TITLES = {
|
|||||||
newFollowersBySourceGraph: 'ChannelStats.Graph.NewFollowersBySource',
|
newFollowersBySourceGraph: 'ChannelStats.Graph.NewFollowersBySource',
|
||||||
languagesGraph: 'ChannelStats.Graph.Language',
|
languagesGraph: 'ChannelStats.Graph.Language',
|
||||||
interactionsGraph: 'ChannelStats.Graph.Interactions',
|
interactionsGraph: 'ChannelStats.Graph.Interactions',
|
||||||
|
reactionsByEmotionGraph: 'ChannelStats.Graph.Reactions',
|
||||||
|
storyInteractionsGraph: 'ChannelStats.Graph.Stories',
|
||||||
|
storyReactionsByEmotionGraph: 'ChannelStats.Graph.StoriesReactions',
|
||||||
};
|
};
|
||||||
const CHANNEL_GRAPHS = Object.keys(CHANNEL_GRAPHS_TITLES) as (keyof ApiChannelStatistics)[];
|
const CHANNEL_GRAPHS = Object.keys(CHANNEL_GRAPHS_TITLES) as (keyof ApiChannelStatistics)[];
|
||||||
|
|
||||||
@ -66,16 +77,22 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
||||||
dcId?: number;
|
dcId?: number;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
|
messagesById: Record<string, ApiMessage>;
|
||||||
|
storiesById?: Record<string, ApiTypeStory>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Statistics: FC<OwnProps & StateProps> = ({
|
const Statistics: FC<OwnProps & StateProps> = ({
|
||||||
chatId,
|
chatId,
|
||||||
|
chat,
|
||||||
statistics,
|
statistics,
|
||||||
dcId,
|
dcId,
|
||||||
isGroup,
|
isGroup,
|
||||||
|
messagesById,
|
||||||
|
storiesById,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -163,7 +180,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
loadedCharts.current.push(name);
|
loadedCharts.current.push(name);
|
||||||
|
|
||||||
containerRef.current!.children[index].classList.remove('hidden');
|
containerRef.current!.children[index].classList.remove(styles.hidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
@ -177,7 +194,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
|
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
|
||||||
<StatisticsOverview
|
<StatisticsOverview
|
||||||
statistics={statistics}
|
statistics={statistics}
|
||||||
type={isGroup ? 'group' : 'channel'}
|
type={isGroup ? 'group' : 'channel'}
|
||||||
@ -188,17 +205,43 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
{graphs.map((graph) => (
|
{graphs.map((graph) => (
|
||||||
<div key={graph} className="Statistics__graph hidden" />
|
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Boolean((statistics as ApiChannelStatistics).recentTopMessages?.length) && (
|
{Boolean((statistics as ApiChannelStatistics).recentPosts?.length) && (
|
||||||
<div className="Statistics__messages">
|
<div className={styles.messages}>
|
||||||
<h2 className="Statistics__messages-title">{lang('ChannelStats.Recent.Header')}</h2>
|
<h2 className={styles.messagesTitle}>{lang('ChannelStats.Recent.Header')}</h2>
|
||||||
|
|
||||||
{(statistics as ApiChannelStatistics).recentTopMessages.map((message) => (
|
{(statistics as ApiChannelStatistics).recentPosts.map((postStatistic) => {
|
||||||
<StatisticsRecentMessage message={message as ApiMessage & StatisticsMessageInteractionCounter} />
|
if ('msgId' in postStatistic) {
|
||||||
))}
|
const message = messagesById[postStatistic.msgId];
|
||||||
|
if (!message || !('content' in message)) return undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatisticsRecentMessage
|
||||||
|
key={`statistic_message_${postStatistic.msgId}`}
|
||||||
|
message={message}
|
||||||
|
postStatistic={postStatistic}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('storyId' in postStatistic && chat) {
|
||||||
|
const story = storiesById?.[postStatistic.storyId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatisticsRecentStory
|
||||||
|
key={`statistic_story_${postStatistic.storyId}`}
|
||||||
|
chat={chat}
|
||||||
|
story={story}
|
||||||
|
postStatistic={postStatistic}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -211,9 +254,11 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const dcId = selectChatFullInfo(global, chatId)?.statisticsDcId;
|
const dcId = selectChatFullInfo(global, chatId)?.statisticsDcId;
|
||||||
const isGroup = chat?.type === 'chatTypeSuperGroup';
|
const isGroup = chat?.type === 'chatTypeSuperGroup';
|
||||||
|
const messagesById = selectChatMessages(global, chatId);
|
||||||
|
const storiesById = selectPeerStories(global, chatId)?.byId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statistics, dcId, isGroup,
|
statistics, dcId, isGroup, chat, messagesById, storiesById,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Statistics));
|
)(Statistics));
|
||||||
|
|||||||
@ -5,36 +5,37 @@ import { getActions } from '../../../global';
|
|||||||
import type { ApiMessagePublicForward } from '../../../api/types';
|
import type { ApiMessagePublicForward } from '../../../api/types';
|
||||||
|
|
||||||
import { getMainUsername } from '../../../global/helpers';
|
import { getMainUsername } from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
|
|
||||||
import './StatisticsPublicForward.scss';
|
import styles from './StatisticsPublicForward.module.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
data: ApiMessagePublicForward;
|
data: ApiMessagePublicForward;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatisticsPublicForward: FC<OwnProps> = ({ data }) => {
|
const StatisticsMessagePublicForward: FC<OwnProps> = ({ data }) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { openChatByUsername } = getActions();
|
const { openChatByUsername } = getActions();
|
||||||
|
|
||||||
const username = useMemo(() => getMainUsername(data.chat), [data.chat]);
|
const username = useMemo(() => (data.chat ? getMainUsername(data.chat) : undefined), [data.chat]);
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
openChatByUsername({ username: username!, messageId: data.messageId });
|
openChatByUsername({ username: username!, messageId: data.messageId });
|
||||||
}, [data.messageId, openChatByUsername, username]);
|
}, [data.messageId, openChatByUsername, username]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="StatisticsPublicForward" onClick={handleClick}>
|
<div className={buildClassName(styles.root, 'statistic-public-forward')} onClick={handleClick}>
|
||||||
<Avatar size="medium" peer={data.chat} />
|
<Avatar size="medium" peer={data.chat} />
|
||||||
|
|
||||||
<div className="StatisticsPublicForward__info">
|
<div>
|
||||||
<div className="StatisticsPublicForward__title">
|
<div className={styles.title}>
|
||||||
{data.title}
|
{data.title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="StatisticsPublicForward__views">
|
<div className={styles.views}>
|
||||||
{lang('ChannelStats.ViewsCount', data.views, 'i')}
|
{lang('ChannelStats.ViewsCount', data.views, 'i')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,4 +43,4 @@ const StatisticsPublicForward: FC<OwnProps> = ({ data }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(StatisticsPublicForward);
|
export default memo(StatisticsMessagePublicForward);
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
.root {
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-right: 2em;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 30px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
|
||||||
|
&:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableCell {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeading {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableValue {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSecondaryValue {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-inline-start: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--color-text-green);
|
||||||
|
|
||||||
|
&.negative {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
.StatisticsOverview {
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid var(--color-borders);
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
margin-right: 2em;
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--text-color);
|
|
||||||
line-height: 30px;
|
|
||||||
text-transform: lowercase;
|
|
||||||
|
|
||||||
&:first-letter {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__caption {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__table {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&-cell {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-heading {
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&-value {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-secondary-value {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
margin-inline-start: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__value {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: var(--color-text-green);
|
|
||||||
|
|
||||||
&.negative {
|
|
||||||
color: var(--color-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@ import React, { memo } from '../../../lib/teact/teact';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiBoostStatistics,
|
ApiBoostStatistics,
|
||||||
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsOverviewItem,
|
ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsOverviewItem,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
@ -12,7 +12,7 @@ import { formatInteger, formatIntegerCompact } from '../../../util/textFormat';
|
|||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import './StatisticsOverview.scss';
|
import styles from './StatisticsOverview.module.scss';
|
||||||
|
|
||||||
type OverviewCell = {
|
type OverviewCell = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -30,7 +30,15 @@ const CHANNEL_OVERVIEW: OverviewCell[][] = [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ name: 'viewsPerPost', title: 'ChannelStats.Overview.ViewsPerPost' },
|
{ name: 'viewsPerPost', title: 'ChannelStats.Overview.ViewsPerPost' },
|
||||||
|
{ name: 'viewsPerStory', title: 'ChannelStats.Overview.ViewsPerStory' },
|
||||||
|
],
|
||||||
|
[
|
||||||
{ name: 'sharesPerPost', title: 'ChannelStats.Overview.SharesPerPost' },
|
{ name: 'sharesPerPost', title: 'ChannelStats.Overview.SharesPerPost' },
|
||||||
|
{ name: 'sharesPerStory', title: 'ChannelStats.Overview.SharesPerStory' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ name: 'reactionsPerPost', title: 'ChannelStats.Overview.ReactionsPerPost' },
|
||||||
|
{ name: 'reactionsPerStory', title: 'ChannelStats.Overview.ReactionsPerStory' },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -47,13 +55,25 @@ const GROUP_OVERVIEW: OverviewCell[][] = [
|
|||||||
|
|
||||||
const MESSAGE_OVERVIEW: OverviewCell[][] = [
|
const MESSAGE_OVERVIEW: OverviewCell[][] = [
|
||||||
[
|
[
|
||||||
{ name: 'views', title: 'Stats.Message.Views', isPlain: true },
|
{ name: 'viewsCount', title: 'Stats.Message.Views', isPlain: true },
|
||||||
{
|
{ name: 'publicForwards', title: 'Stats.Message.PublicShares', isPlain: true },
|
||||||
name: 'forwards', title: 'Stats.Message.PrivateShares', isPlain: true, isApproximate: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ name: 'publicForwards', title: 'Stats.Message.PublicShares', isPlain: true },
|
{ name: 'reactionsCount', title: 'Channel.Stats.Overview.Reactions', isPlain: true },
|
||||||
|
{
|
||||||
|
name: 'forwardsCount', title: 'Stats.Message.PrivateShares', isPlain: true, isApproximate: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const STORY_OVERVIEW: OverviewCell[][] = [
|
||||||
|
[
|
||||||
|
{ name: 'viewsCount', title: 'Channel.Stats.Overview.Views', isPlain: true },
|
||||||
|
{ name: 'publicForwards', title: 'PublicShares', isPlain: true },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ name: 'reactionsCount', title: 'Channel.Stats.Overview.Reactions', isPlain: true },
|
||||||
|
{ name: 'forwardsCount', title: 'PrivateShares', isPlain: true },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -74,13 +94,13 @@ const BOOST_OVERVIEW: OverviewCell[][] = [
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
type StatisticsType = 'channel' | 'group' | 'message' | 'boost';
|
type StatisticsType = 'channel' | 'group' | 'message' | 'boost' | 'story';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
type: StatisticsType;
|
type: StatisticsType;
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics | ApiBoostStatistics;
|
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiPostStatistics | ApiBoostStatistics;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatisticsOverview: FC<OwnProps> = ({
|
const StatisticsOverview: FC<OwnProps> = ({
|
||||||
@ -99,7 +119,7 @@ const StatisticsOverview: FC<OwnProps> = ({
|
|||||||
const isChangeNegative = Number(change) < 0;
|
const isChangeNegative = Number(change) < 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={buildClassName('StatisticsOverview__value', isChangeNegative && 'negative')}>
|
<span className={buildClassName(styles.value, isChangeNegative && styles.negative)}>
|
||||||
{isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`}
|
{isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`}
|
||||||
{percentage && (
|
{percentage && (
|
||||||
<>
|
<>
|
||||||
@ -113,26 +133,25 @@ const StatisticsOverview: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const { period } = (statistics as ApiGroupStatistics);
|
const { period } = (statistics as ApiGroupStatistics);
|
||||||
|
|
||||||
const schema = type === 'boost' ? BOOST_OVERVIEW : type === 'message' ? MESSAGE_OVERVIEW : type === 'group'
|
const schema = getSchemaByType(type);
|
||||||
? GROUP_OVERVIEW : CHANNEL_OVERVIEW;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('StatisticsOverview', className)}>
|
<div className={buildClassName(styles.root, className)}>
|
||||||
<div className="StatisticsOverview__header">
|
<div className={styles.header}>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="StatisticsOverview__title">
|
<div className={styles.title}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{period && (
|
{period && (
|
||||||
<div className="StatisticsOverview__caption">
|
<div className={styles.caption}>
|
||||||
{formatFullDate(lang, period.minDate * 1000)} — {formatFullDate(lang, period.maxDate * 1000)}
|
{formatFullDate(lang, period.minDate * 1000)} — {formatFullDate(lang, period.maxDate * 1000)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table className="StatisticsOverview__table">
|
<table className={styles.table}>
|
||||||
{schema.map((row) => (
|
{schema.map((row) => (
|
||||||
<tr>
|
<tr>
|
||||||
{row.map((cell: OverviewCell) => {
|
{row.map((cell: OverviewCell) => {
|
||||||
@ -140,39 +159,39 @@ const StatisticsOverview: FC<OwnProps> = ({
|
|||||||
|
|
||||||
if (cell.isPlain) {
|
if (cell.isPlain) {
|
||||||
return (
|
return (
|
||||||
<td className="StatisticsOverview__table-cell">
|
<td className={styles.tableCell}>
|
||||||
<b className="StatisticsOverview__table-value">
|
<b className={styles.tableValue}>
|
||||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
|
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
|
||||||
</b>
|
</b>
|
||||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cell.isPercentage) {
|
if (cell.isPercentage) {
|
||||||
return (
|
return (
|
||||||
<td className="StatisticsOverview__table-cell">
|
<td className={styles.tableCell}>
|
||||||
{cell.withAbsoluteValue && (
|
{cell.withAbsoluteValue && (
|
||||||
<span className="StatisticsOverview__table-value">
|
<span className={styles.tableValue}>
|
||||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`}
|
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={`StatisticsOverview__table-${cell.withAbsoluteValue ? 'secondary-' : ''}value`}>
|
<span className={cell.withAbsoluteValue ? styles.tableSecondaryValue : styles.tableValue}>
|
||||||
{field.percentage}%
|
{field.percentage}%
|
||||||
</span>
|
</span>
|
||||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className="StatisticsOverview__table-cell">
|
<td className={styles.tableCell}>
|
||||||
<b className="StatisticsOverview__table-value">
|
<b className={styles.tableValue}>
|
||||||
{formatIntegerCompact(field.current)}
|
{formatIntegerCompact(field.current)}
|
||||||
</b>
|
</b>
|
||||||
{' '}
|
{' '}
|
||||||
{renderOverviewItemValue(field)}
|
{renderOverviewItemValue(field)}
|
||||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -183,4 +202,20 @@ const StatisticsOverview: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getSchemaByType(type: StatisticsType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'group':
|
||||||
|
return GROUP_OVERVIEW;
|
||||||
|
case 'message':
|
||||||
|
return MESSAGE_OVERVIEW;
|
||||||
|
case 'boost':
|
||||||
|
return BOOST_OVERVIEW;
|
||||||
|
case 'story':
|
||||||
|
return STORY_OVERVIEW;
|
||||||
|
case 'channel':
|
||||||
|
default:
|
||||||
|
return CHANNEL_OVERVIEW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(StatisticsOverview);
|
export default memo(StatisticsOverview);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.StatisticsPublicForward {
|
.root {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
@ -9,17 +9,17 @@
|
|||||||
background-color: var(--color-chat-hover);
|
background-color: var(--color-chat-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Avatar {
|
:global(.Avatar) {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
&__title {
|
|
||||||
line-height: 1.25rem;
|
.title {
|
||||||
}
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
&__views {
|
|
||||||
color: var(--color-text-meta);
|
.views {
|
||||||
font-size: 0.8125rem;
|
color: var(--color-text-meta);
|
||||||
}
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
@ -1,68 +0,0 @@
|
|||||||
.StatisticsRecentMessage {
|
|
||||||
position: relative;
|
|
||||||
cursor: var(--custom-cursor, pointer);
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: var(--color-chat-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--with-image {
|
|
||||||
padding-left: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__summary {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
|
|
||||||
.media-preview__image {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
position: absolute;
|
|
||||||
left: 0.5rem;
|
|
||||||
top: 0.5rem;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin-inline-end: 0.25rem;
|
|
||||||
|
|
||||||
&.round {
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-play {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #fff;
|
|
||||||
margin-inline-start: -1.25rem;
|
|
||||||
margin-inline-end: 0.5rem;
|
|
||||||
bottom: 0.0625rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--color-text-meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__meta {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__date {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -18,13 +18,17 @@ import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
|||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
|
||||||
import './StatisticsRecentMessage.scss';
|
import Icon from '../../common/Icon';
|
||||||
|
import StatisticsRecentPostMeta from './StatisticsRecentPostMeta';
|
||||||
|
|
||||||
|
import styles from './StatisticsRecentPost.module.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
message: ApiMessage & StatisticsMessageInteractionCounter;
|
postStatistic: StatisticsMessageInteractionCounter;
|
||||||
|
message: ApiMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
const StatisticsRecentMessage: FC<OwnProps> = ({ postStatistic, message }) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { toggleMessageStatistics } = getActions();
|
const { toggleMessageStatistics } = getActions();
|
||||||
|
|
||||||
@ -39,27 +43,25 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'StatisticsRecentMessage',
|
styles.root,
|
||||||
Boolean(mediaBlobUrl || mediaThumbnail) && 'StatisticsRecentMessage--with-image',
|
Boolean(mediaBlobUrl || mediaThumbnail) && styles.withImage,
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div className="StatisticsRecentMessage__title">
|
<div className={styles.title}>
|
||||||
<div className="StatisticsRecentMessage__summary">
|
<div className={styles.summary}>
|
||||||
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||||
</div>
|
</div>
|
||||||
<div className="StatisticsRecentMessage__meta">
|
<div className={styles.meta}>
|
||||||
{lang('ChannelStats.ViewsCount', message.views, 'i')}
|
{lang('ChannelStats.ViewsCount', postStatistic.viewsCount, 'i')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="StatisticsRecentMessage__info">
|
<div className={styles.info}>
|
||||||
<div className="StatisticsRecentMessage__date">
|
<div className={styles.date}>
|
||||||
{formatDateTimeToString(message.date * 1000, lang.code)}
|
{formatDateTimeToString(message.date * 1000, lang.code)}
|
||||||
</div>
|
</div>
|
||||||
<div className="StatisticsRecentMessage__meta">
|
<StatisticsRecentPostMeta postStatistic={postStatistic} />
|
||||||
{message.forwards ? lang('ChannelStats.SharesCount', message.forwards) : 'No shares'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -71,14 +73,14 @@ function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="media-preview">
|
<span>
|
||||||
<img
|
<img
|
||||||
src={blobUrl}
|
src={blobUrl}
|
||||||
alt=""
|
alt=""
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className={buildClassName('media-preview__image', isRoundVideo && 'round')}
|
className={buildClassName(styles.image, isRoundVideo && styles.round)}
|
||||||
/>
|
/>
|
||||||
{getMessageVideo(message) && <i className="icon icon-play" />}
|
{getMessageVideo(message) && <Icon name="play" />}
|
||||||
{renderMessageSummary(lang, message, true)}
|
{renderMessageSummary(lang, message, true)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
127
src/components/right/statistics/StatisticsRecentPost.module.scss
Normal file
127
src/components/right/statistics/StatisticsRecentPost.module.scss
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withImage {
|
||||||
|
padding-left: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
left: -0.25rem;
|
||||||
|
top: -0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-borders-read-story);
|
||||||
|
background-image: linear-gradient(215.87deg, var(--color-avatar-story-unread-from) -1.61%, var(--color-avatar-story-unread-to) 97.44%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
left: -0.125rem;
|
||||||
|
top: -0.125rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 0;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
|
||||||
|
&.round {
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.Avatar) {
|
||||||
|
left: 0.375rem;
|
||||||
|
top: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.withStoryCircle {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
|
||||||
|
:global(.icon-play) {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-inline-start: -1.25rem;
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
bottom: 0.0625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--color-text-meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaWithIcon {
|
||||||
|
margin-inline-start: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaIcon {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
vertical-align: -0.125rem;
|
||||||
|
margin-inline-end: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
41
src/components/right/statistics/StatisticsRecentPostMeta.tsx
Normal file
41
src/components/right/statistics/StatisticsRecentPostMeta.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { StatisticsMessageInteractionCounter, StatisticsStoryInteractionCounter } from '../../../api/types';
|
||||||
|
|
||||||
|
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import Icon from '../../common/Icon';
|
||||||
|
|
||||||
|
import styles from './StatisticsRecentPost.module.scss';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
postStatistic: StatisticsStoryInteractionCounter | StatisticsMessageInteractionCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatisticsRecentPostMeta({ postStatistic }: OwnProps) {
|
||||||
|
const lang = useLang();
|
||||||
|
return (
|
||||||
|
<div className={styles.meta}>
|
||||||
|
{postStatistic.reactionsCount > 0 && (
|
||||||
|
<span className={styles.metaWithIcon}>
|
||||||
|
<Icon name="heart-outline" className={styles.metaIcon} />
|
||||||
|
{formatIntegerCompact(postStatistic.reactionsCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{postStatistic.forwardsCount > 0 && (
|
||||||
|
<span className={styles.metaWithIcon}>
|
||||||
|
<Icon name="forward" className={styles.metaIcon} />
|
||||||
|
{formatIntegerCompact(postStatistic.forwardsCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!postStatistic.forwardsCount && !postStatistic.reactionsCount
|
||||||
|
&& lang('ChannelStats.SharesCount_ZeroValueHolder')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StatisticsRecentPostMeta);
|
||||||
95
src/components/right/statistics/StatisticsRecentStory.tsx
Normal file
95
src/components/right/statistics/StatisticsRecentStory.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiChat,
|
||||||
|
ApiTypeStory,
|
||||||
|
StatisticsStoryInteractionCounter,
|
||||||
|
} from '../../../api/types';
|
||||||
|
import type { LangFn } from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import { getStoryMediaHash } from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatDateTimeToString } from '../../../util/dateFormat';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import StatisticsRecentPostMeta from './StatisticsRecentPostMeta';
|
||||||
|
|
||||||
|
import styles from './StatisticsRecentPost.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
chat: ApiChat;
|
||||||
|
story?: ApiTypeStory;
|
||||||
|
postStatistic: StatisticsStoryInteractionCounter;
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatisticsRecentStory({ chat, story, postStatistic }: OwnProps) {
|
||||||
|
const lang = useLang();
|
||||||
|
const { toggleStoryStatistics } = getActions();
|
||||||
|
const isLoaded = story && 'content' in story;
|
||||||
|
|
||||||
|
const video = isLoaded ? story.content.video : undefined;
|
||||||
|
const imageHash = isLoaded ? getStoryMediaHash(story) : undefined;
|
||||||
|
const imgBlobUrl = useMedia(imageHash);
|
||||||
|
const mediaThumbnail = imgBlobUrl || video?.thumbnail?.dataUri;
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
toggleStoryStatistics({ storyId: postStatistic.storyId });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.root, styles.withImage)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<div className={styles.summary}>
|
||||||
|
{renderSummary(lang, chat, imgBlobUrl || mediaThumbnail)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.meta}>
|
||||||
|
{lang('ChannelStats.ViewsCount', postStatistic.viewsCount, 'i')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{isLoaded && Boolean(story.date) && formatDateTimeToString(story.date * 1000, lang.code)}
|
||||||
|
</div>
|
||||||
|
<StatisticsRecentPostMeta postStatistic={postStatistic} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(lang: LangFn, chat: ApiChat, blobUrl?: string) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{blobUrl ? (
|
||||||
|
<span className={styles.imageContainer}>
|
||||||
|
<img
|
||||||
|
src={blobUrl}
|
||||||
|
alt=""
|
||||||
|
draggable={false}
|
||||||
|
className={buildClassName(styles.image, styles.circle, styles.withStoryCircle)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
peer={chat}
|
||||||
|
size="small-mobile"
|
||||||
|
className={styles.image}
|
||||||
|
withStorySolid
|
||||||
|
forceUnreadStorySolid
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{lang('Story')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StatisticsRecentStory);
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiChat, ApiStoryPublicForward, ApiUser,
|
||||||
|
} from '../../../api/types';
|
||||||
|
|
||||||
|
import { getChatTitle, getUserFullName } from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
|
||||||
|
import styles from './StatisticsPublicForward.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
data: ApiStoryPublicForward;
|
||||||
|
usersById: Record<string, ApiUser>;
|
||||||
|
chatsById: Record<string, ApiChat>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatisticsMessagePublicForward({ data, chatsById, usersById }: OwnProps) {
|
||||||
|
const lang = useLang();
|
||||||
|
const { openChat } = getActions();
|
||||||
|
const user = usersById[data.peerId];
|
||||||
|
const chat = chatsById[data.peerId];
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
openChat({ id: user.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, 'statistic-public-forward')} onClick={handleClick}>
|
||||||
|
<Avatar size="medium" peer={user || chat} withStorySolid forceUnreadStorySolid />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{user ? getUserFullName(user) : getChatTitle(lang, chat)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.views}>
|
||||||
|
{data.viewsCount ? lang('ChannelStats.ViewsCount', data.viewsCount, 'i') : lang('NoViews')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StatisticsMessagePublicForward);
|
||||||
19
src/components/right/statistics/StoryStatistics.async.tsx
Normal file
19
src/components/right/statistics/StoryStatistics.async.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { OwnProps } from './StoryStatistics';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
import Loading from '../../ui/Loading';
|
||||||
|
|
||||||
|
const StoryStatisticsAsync: FC<OwnProps> = (props) => {
|
||||||
|
const StoryStatistics = useModuleLoader(Bundles.Extra, 'StoryStatistics');
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return StoryStatistics ? <StoryStatistics {...props} /> : <Loading />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StoryStatisticsAsync;
|
||||||
228
src/components/right/statistics/StoryStatistics.tsx
Normal file
228
src/components/right/statistics/StoryStatistics.tsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useEffect, useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
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 useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
import Loading from '../../ui/Loading';
|
||||||
|
import StatisticsMessagePublicForward from './StatisticsMessagePublicForward';
|
||||||
|
import StatisticsOverview from './StatisticsOverview';
|
||||||
|
import StatisticsStoryPublicForward from './StatisticsStoryPublicForward';
|
||||||
|
|
||||||
|
import styles from './Statistics.module.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.StoryInteractionsTitle',
|
||||||
|
reactionsGraph: 'ReactionsByEmotionChartTitle',
|
||||||
|
};
|
||||||
|
const GRAPHS = Object.keys(GRAPH_TITLES) as (keyof ApiPostStatistics)[];
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StateProps = {
|
||||||
|
statistics?: ApiPostStatistics;
|
||||||
|
storyId?: number;
|
||||||
|
dcId?: number;
|
||||||
|
chatsById: Record<string, ApiChat>;
|
||||||
|
usersById: Record<string, ApiUser>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function StoryStatistics({
|
||||||
|
chatId,
|
||||||
|
isActive,
|
||||||
|
statistics,
|
||||||
|
dcId,
|
||||||
|
storyId,
|
||||||
|
chatsById,
|
||||||
|
usersById,
|
||||||
|
}: OwnProps & StateProps) {
|
||||||
|
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 { loadStoryStatistics, loadStoryPublicForwards, loadStatisticsAsyncGraph } = getActions();
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (storyId) {
|
||||||
|
loadStoryStatistics({ chatId, storyId });
|
||||||
|
}
|
||||||
|
}, [chatId, storyId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive || storyId) {
|
||||||
|
loadedCharts.current = [];
|
||||||
|
setIsReady(false);
|
||||||
|
}
|
||||||
|
}, [isActive, storyId]);
|
||||||
|
|
||||||
|
// 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 || !containerRef.current) {
|
||||||
|
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, storyId, loadStatisticsAsyncGraph, dcId, forceUpdate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleLoadMore = useLastCallback(() => {
|
||||||
|
if (!storyId) return;
|
||||||
|
|
||||||
|
loadStoryPublicForwards({ chatId, storyId });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isReady || !statistics || !storyId) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
|
||||||
|
<StatisticsOverview statistics={statistics} type="story" title={lang('StatisticOverview')} />
|
||||||
|
|
||||||
|
{!loadedCharts.current.length && <Loading />}
|
||||||
|
|
||||||
|
<div ref={containerRef}>
|
||||||
|
{GRAPHS.map((graph) => (
|
||||||
|
<div className={buildClassName(styles.graph, !loadedCharts.current.includes(graph) && styles.hidden)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Boolean(statistics.publicForwards) && (
|
||||||
|
<div className={styles.publicForwards}>
|
||||||
|
<h2 className={styles.publicForwardsTitle}>{lang('Stats.Message.PublicShares')}</h2>
|
||||||
|
|
||||||
|
<InfiniteScroll
|
||||||
|
items={statistics.publicForwardsData}
|
||||||
|
itemSelector=".statistic-public-forward"
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
preloadBackwards={STATISTICS_PUBLIC_FORWARDS_LIMIT}
|
||||||
|
noFastList
|
||||||
|
>
|
||||||
|
{statistics.publicForwardsData!.map((item) => {
|
||||||
|
if ('messageId' in item) {
|
||||||
|
return (
|
||||||
|
<StatisticsMessagePublicForward key={`message_${item.messageId}`} data={item} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatisticsStoryPublicForward
|
||||||
|
key={`story_${item.storyId}`}
|
||||||
|
data={item}
|
||||||
|
chatsById={chatsById}
|
||||||
|
usersById={usersById}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</InfiniteScroll>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const dcId = selectChatFullInfo(global, chatId)?.statisticsDcId;
|
||||||
|
const tabState = selectTabState(global);
|
||||||
|
const statistics = tabState.statistics.currentStory;
|
||||||
|
const storyId = tabState.statistics.currentStoryId;
|
||||||
|
const { byId: usersById } = global.users;
|
||||||
|
const { byId: chatsById } = global.chats;
|
||||||
|
|
||||||
|
return {
|
||||||
|
statistics, dcId, storyId, usersById, chatsById,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(StoryStatistics));
|
||||||
@ -32,8 +32,9 @@ const StoryFooter = ({
|
|||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
viewsCount, forwardsCount, reactionsCount, isOut, peerId, id: storyId, sentReaction,
|
views, isOut, peerId, id: storyId, sentReaction,
|
||||||
} = story;
|
} = story;
|
||||||
|
const { viewsCount, forwardsCount, reactionsCount } = views || {};
|
||||||
const isChannel = !isUserId(peerId);
|
const isChannel = !isUserId(peerId);
|
||||||
|
|
||||||
const isSentStoryReactionHeart = sentReaction && 'emoticon' in sentReaction
|
const isSentStoryReactionHeart = sentReaction && 'emoticon' in sentReaction
|
||||||
@ -50,11 +51,11 @@ const StoryFooter = ({
|
|||||||
const recentViewers = useMemo(() => {
|
const recentViewers = useMemo(() => {
|
||||||
const { users: { byId: usersById } } = getGlobal();
|
const { users: { byId: usersById } } = getGlobal();
|
||||||
|
|
||||||
const recentViewerIds = story && 'recentViewerIds' in story ? story.recentViewerIds : undefined;
|
const recentViewerIds = views && 'recentViewerIds' in views ? views.recentViewerIds : undefined;
|
||||||
if (!recentViewerIds) return undefined;
|
if (!recentViewerIds) return undefined;
|
||||||
|
|
||||||
return recentViewerIds.map((id) => usersById[id]).filter(Boolean);
|
return recentViewerIds.map((id) => usersById[id]).filter(Boolean);
|
||||||
}, [story]);
|
}, [views]);
|
||||||
|
|
||||||
const handleOpenStoryViewModal = useLastCallback(() => {
|
const handleOpenStoryViewModal = useLastCallback(() => {
|
||||||
openStoryViewModal({ storyId });
|
openStoryViewModal({ storyId });
|
||||||
|
|||||||
@ -69,8 +69,7 @@ function StoryViewModal({
|
|||||||
|
|
||||||
const isOpen = Boolean(story);
|
const isOpen = Boolean(story);
|
||||||
const isExpired = Boolean(story?.date) && (story!.date + viewersExpirePeriod) < getServerTime();
|
const isExpired = Boolean(story?.date) && (story!.date + viewersExpirePeriod) < getServerTime();
|
||||||
const viewsCount = story?.viewsCount || 0;
|
const { viewsCount = 0, reactionsCount = 0 } = story?.views || {};
|
||||||
const reactionsCount = story?.reactionsCount || 0;
|
|
||||||
|
|
||||||
const shouldShowJustContacts = story?.isPublic && viewsCount > STORY_VIEWS_MIN_CONTACTS_FILTER;
|
const shouldShowJustContacts = story?.isPublic && viewsCount > STORY_VIEWS_MIN_CONTACTS_FILTER;
|
||||||
const shouldShowSortByReactions = reactionsCount > STORY_MIN_REACTIONS_SORT;
|
const shouldShowSortByReactions = reactionsCount > STORY_MIN_REACTIONS_SORT;
|
||||||
|
|||||||
@ -44,7 +44,8 @@ const MediaAreaSuggestedReaction = ({
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [customEmojiSize, setCustomEmojiSize] = useState(1.5 * REM);
|
const [customEmojiSize, setCustomEmojiSize] = useState(1.5 * REM);
|
||||||
|
|
||||||
const { peerId, id, reactions } = story;
|
const { peerId, id, views } = story;
|
||||||
|
const { reactions } = views || {};
|
||||||
const { reaction, isDark, isFlipped } = mediaArea;
|
const { reaction, isDark, isFlipped } = mediaArea;
|
||||||
|
|
||||||
const isChannel = !isUserId(peerId);
|
const isChannel = !isUserId(peerId);
|
||||||
|
|||||||
@ -90,6 +90,7 @@ export const COMMON_CHATS_LIMIT = 100;
|
|||||||
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
||||||
export const STORY_LIST_LIMIT = 100;
|
export const STORY_LIST_LIMIT = 100;
|
||||||
export const API_GENERAL_ID_LIMIT = 100;
|
export const API_GENERAL_ID_LIMIT = 100;
|
||||||
|
export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50;
|
||||||
|
|
||||||
export const STORY_VIEWS_MIN_SEARCH = 15;
|
export const STORY_VIEWS_MIN_SEARCH = 15;
|
||||||
export const STORY_MIN_REACTIONS_SORT = 10;
|
export const STORY_MIN_REACTIONS_SORT = 10;
|
||||||
|
|||||||
@ -1776,8 +1776,8 @@ addActionHandler('loadMessageViews', async (global, actions, payload): Promise<v
|
|||||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||||
result.viewsInfo.forEach((update) => {
|
result.viewsInfo.forEach((update) => {
|
||||||
global = updateChatMessage(global, chatId, update.id, {
|
global = updateChatMessage(global, chatId, update.id, {
|
||||||
views: update.views,
|
viewsCount: update.views,
|
||||||
forwards: update.forwards,
|
forwardsCount: update.forwards,
|
||||||
});
|
});
|
||||||
|
|
||||||
global = updateThreadInfo(global, chatId, update.id, update.threadInfo);
|
global = updateThreadInfo(global, chatId, update.id, update.threadInfo);
|
||||||
|
|||||||
@ -1,12 +1,23 @@
|
|||||||
|
import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
import {
|
import {
|
||||||
|
addChats,
|
||||||
addUsers,
|
addUsers,
|
||||||
updateMessageStatistics, updateStatistics, updateStatisticsGraph,
|
updateMessageStatistics,
|
||||||
|
updateStatistics,
|
||||||
|
updateStatisticsGraph,
|
||||||
|
updateStoryStatistics,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import { selectChat, selectChatFullInfo, selectChatMessages } from '../../selectors';
|
import {
|
||||||
|
selectChat,
|
||||||
|
selectChatFullInfo,
|
||||||
|
selectChatMessages,
|
||||||
|
selectPeerStory,
|
||||||
|
selectTabState,
|
||||||
|
} from '../../selectors';
|
||||||
|
|
||||||
addActionHandler('loadStatistics', async (global, actions, payload): Promise<void> => {
|
addActionHandler('loadStatistics', async (global, actions, payload): Promise<void> => {
|
||||||
const { chatId, isGroup, tabId = getCurrentTabId() } = payload;
|
const { chatId, isGroup, tabId = getCurrentTabId() } = payload;
|
||||||
@ -28,13 +39,6 @@ addActionHandler('loadStatistics', async (global, actions, payload): Promise<voi
|
|||||||
const { stats, users } = result;
|
const { stats, users } = result;
|
||||||
|
|
||||||
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
|
|
||||||
if ('recentTopMessages' in stats && stats.recentTopMessages.length) {
|
|
||||||
const messages = selectChatMessages(global, chatId);
|
|
||||||
|
|
||||||
stats.recentTopMessages = stats.recentTopMessages.map((message) => ({ ...message, ...messages[message.msgId] }));
|
|
||||||
}
|
|
||||||
|
|
||||||
global = updateStatistics(global, chatId, stats, tabId);
|
global = updateStatistics(global, chatId, stats, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
@ -55,18 +59,63 @@ addActionHandler('loadMessageStatistics', async (global, actions, payload): Prom
|
|||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
|
|
||||||
const { views, forwards } = selectChatMessages(global, chatId)[messageId];
|
const {
|
||||||
result.views = views;
|
viewsCount,
|
||||||
result.forwards = forwards;
|
forwardsCount,
|
||||||
|
reactions,
|
||||||
const publicForwards = await callApi('fetchMessagePublicForwards', { chat, messageId, dcId });
|
} = selectChatMessages(global, chatId)[messageId] || {};
|
||||||
result.publicForwards = publicForwards?.length;
|
result.viewsCount = viewsCount;
|
||||||
result.publicForwardsData = publicForwards;
|
result.forwardsCount = forwardsCount;
|
||||||
|
result.reactionsCount = reactions?.results
|
||||||
global = getGlobal();
|
? reactions?.results.reduce((acc, reaction) => acc + reaction.count, 0)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
global = updateMessageStatistics(global, result, tabId);
|
global = updateMessageStatistics(global, result, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
actions.loadMessagePublicForwards({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadMessagePublicForwards', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const fullInfo = selectChatFullInfo(global, chatId);
|
||||||
|
if (!chat || !fullInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcId = fullInfo.statisticsDcId;
|
||||||
|
const stats = selectTabState(global, tabId).statistics.currentMessage || {};
|
||||||
|
|
||||||
|
if (stats?.publicForwards && !stats.nextRate) return;
|
||||||
|
|
||||||
|
const publicForwards = await callApi('fetchMessagePublicForwards', {
|
||||||
|
chat, messageId, dcId, offsetRate: stats?.nextRate,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
forwards,
|
||||||
|
nextRate,
|
||||||
|
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 || []),
|
||||||
|
),
|
||||||
|
nextRate,
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadStatisticsAsyncGraph', async (global, actions, payload): Promise<void> => {
|
addActionHandler('loadStatisticsAsyncGraph', async (global, actions, payload): Promise<void> => {
|
||||||
@ -89,3 +138,80 @@ addActionHandler('loadStatisticsAsyncGraph', async (global, actions, payload): P
|
|||||||
global = updateStatisticsGraph(global, chatId, name, result, tabId);
|
global = updateStatisticsGraph(global, chatId, name, result, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadStoryStatistics', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { chatId, storyId, tabId = getCurrentTabId() } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const fullInfo = selectChatFullInfo(global, chatId);
|
||||||
|
if (!chat || !fullInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcId = fullInfo.statisticsDcId;
|
||||||
|
let result = await callApi('fetchStoryStatistics', { chat, storyId, dcId });
|
||||||
|
if (!result) {
|
||||||
|
result = {};
|
||||||
|
}
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
const story = selectPeerStory(global, chatId, storyId);
|
||||||
|
const {
|
||||||
|
forwardsCount = 0,
|
||||||
|
viewsCount = 0,
|
||||||
|
reactionsCount = 0,
|
||||||
|
} = story && 'views' in story && story.views ? story.views : {};
|
||||||
|
result.viewsCount = viewsCount;
|
||||||
|
result.forwardsCount = forwardsCount;
|
||||||
|
result.reactionsCount = reactionsCount;
|
||||||
|
global = getGlobal();
|
||||||
|
global = updateStoryStatistics(global, result, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
actions.loadStoryPublicForwards({
|
||||||
|
chatId,
|
||||||
|
storyId,
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadStoryPublicForwards', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { chatId, storyId, tabId = getCurrentTabId() } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const fullInfo = selectChatFullInfo(global, chatId);
|
||||||
|
if (!chat || !fullInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dcId = fullInfo.statisticsDcId;
|
||||||
|
const stats = selectTabState(global, tabId).statistics.currentStory || {};
|
||||||
|
|
||||||
|
if (stats?.publicForwards && !stats.nextOffsetId) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
publicForwards,
|
||||||
|
users,
|
||||||
|
chats,
|
||||||
|
count,
|
||||||
|
nextOffsetId,
|
||||||
|
} = await callApi('fetchStoryPublicForwards', {
|
||||||
|
chat, storyId, dcId, offsetId: stats.nextOffsetId,
|
||||||
|
}) || {};
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
if (chats) {
|
||||||
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
||||||
|
}
|
||||||
|
if (users) {
|
||||||
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
|
}
|
||||||
|
global = updateStoryStatistics(global, {
|
||||||
|
...stats,
|
||||||
|
publicForwards: count || publicForwards?.length,
|
||||||
|
publicForwardsData: (stats.publicForwardsData || []).concat(
|
||||||
|
publicForwards || [],
|
||||||
|
),
|
||||||
|
nextOffsetId,
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
updatePeerPinnedStory,
|
updatePeerPinnedStory,
|
||||||
updatePeerStoriesHidden,
|
updatePeerStoriesHidden,
|
||||||
updatePeerStory,
|
updatePeerStory,
|
||||||
|
updatePeerStoryViews,
|
||||||
updatePeersWithStories,
|
updatePeersWithStories,
|
||||||
updateSentStoryReaction,
|
updateSentStoryReaction,
|
||||||
updateStealthMode,
|
updateStealthMode,
|
||||||
@ -360,7 +361,7 @@ addActionHandler('loadStoryViews', async (global, actions, payload): Promise<voi
|
|||||||
|
|
||||||
if (isPreload && result.views?.length) {
|
if (isPreload && result.views?.length) {
|
||||||
const recentViewerIds = result.views.map((view) => view.userId);
|
const recentViewerIds = result.views.map((view) => view.userId);
|
||||||
global = updatePeerStory(global, peerId, storyId, {
|
global = updatePeerStoryViews(global, peerId, storyId, {
|
||||||
recentViewerIds,
|
recentViewerIds,
|
||||||
viewsCount: result.viewsCount,
|
viewsCount: result.viewsCount,
|
||||||
reactionsCount: result.reactionsCount,
|
reactionsCount: result.reactionsCount,
|
||||||
|
|||||||
@ -162,6 +162,7 @@ addActionHandler('toggleStatistics', (global, actions, payload): ActionReturnTyp
|
|||||||
statistics: {
|
statistics: {
|
||||||
...tabState.statistics,
|
...tabState.statistics,
|
||||||
currentMessageId: undefined,
|
currentMessageId: undefined,
|
||||||
|
currentStoryId: undefined,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
@ -172,6 +173,18 @@ addActionHandler('toggleMessageStatistics', (global, actions, payload): ActionRe
|
|||||||
statistics: {
|
statistics: {
|
||||||
...selectTabState(global, tabId).statistics,
|
...selectTabState(global, tabId).statistics,
|
||||||
currentMessageId: messageId,
|
currentMessageId: messageId,
|
||||||
|
currentStoryId: undefined,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('toggleStoryStatistics', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId(), storyId } = payload || {};
|
||||||
|
return updateTabState(global, {
|
||||||
|
statistics: {
|
||||||
|
...selectTabState(global, tabId).statistics,
|
||||||
|
currentStoryId: storyId,
|
||||||
|
currentMessageId: undefined,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsGraph,
|
ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsGraph,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { GlobalState, TabArgs } from '../types';
|
import type { GlobalState, TabArgs } from '../types';
|
||||||
|
|
||||||
@ -22,13 +22,27 @@ export function updateStatistics<T extends GlobalState>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateMessageStatistics<T extends GlobalState>(
|
export function updateMessageStatistics<T extends GlobalState>(
|
||||||
global: T, statistics: ApiMessageStatistics,
|
global: T, statistics: ApiPostStatistics,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
statistics: {
|
statistics: {
|
||||||
...selectTabState(global, tabId).statistics,
|
...selectTabState(global, tabId).statistics,
|
||||||
currentMessage: statistics,
|
currentMessage: statistics,
|
||||||
|
currentStory: undefined,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateStoryStatistics<T extends GlobalState>(
|
||||||
|
global: T, statistics: ApiPostStatistics,
|
||||||
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
|
): T {
|
||||||
|
return updateTabState(global, {
|
||||||
|
statistics: {
|
||||||
|
...selectTabState(global, tabId).statistics,
|
||||||
|
currentStory: statistics,
|
||||||
|
currentMessage: undefined,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type {
|
|||||||
ApiStoryDeleted,
|
ApiStoryDeleted,
|
||||||
ApiStorySkipped,
|
ApiStorySkipped,
|
||||||
ApiStoryView,
|
ApiStoryView,
|
||||||
|
ApiStoryViews,
|
||||||
ApiTypeStory,
|
ApiTypeStory,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { GlobalState, TabArgs } from '../types';
|
import type { GlobalState, TabArgs } from '../types';
|
||||||
@ -317,17 +318,21 @@ export function updateSentStoryReaction<T extends GlobalState>(
|
|||||||
const story = selectPeerStory(global, peerId, storyId);
|
const story = selectPeerStory(global, peerId, storyId);
|
||||||
if (!story || !('content' in story)) return global;
|
if (!story || !('content' in story)) return global;
|
||||||
|
|
||||||
const reactionsCount = story.reactionsCount || 0;
|
const { views } = story;
|
||||||
const hasReaction = story.reactions?.some((r) => r.chosenOrder !== undefined);
|
const reactionsCount = views?.reactionsCount || 0;
|
||||||
const reactions = updateReactionCount(story.reactions || [], [reaction].filter(Boolean));
|
const hasReaction = views?.reactions?.some((r) => r.chosenOrder !== undefined);
|
||||||
|
const reactions = updateReactionCount(views?.reactions || [], [reaction].filter(Boolean));
|
||||||
|
|
||||||
const countDiff = !reaction ? -1 : hasReaction ? 0 : 1;
|
const countDiff = !reaction ? -1 : hasReaction ? 0 : 1;
|
||||||
const newReactionsCount = reactionsCount + countDiff;
|
const newReactionsCount = reactionsCount + countDiff;
|
||||||
|
|
||||||
global = updatePeerStory(global, peerId, storyId, {
|
global = updatePeerStory(global, peerId, storyId, {
|
||||||
sentReaction: reaction,
|
sentReaction: reaction,
|
||||||
reactionsCount: newReactionsCount,
|
views: {
|
||||||
reactions,
|
...views,
|
||||||
|
reactionsCount: newReactionsCount,
|
||||||
|
reactions,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return global;
|
return global;
|
||||||
@ -364,6 +369,25 @@ export function updatePeerStory<T extends GlobalState>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updatePeerStoryViews<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
peerId: string,
|
||||||
|
storyId: number,
|
||||||
|
viewsUpdate: Partial<ApiStoryViews>,
|
||||||
|
): T {
|
||||||
|
const story = selectPeerStory(global, peerId, storyId);
|
||||||
|
if (!story || !('content' in story)) return global;
|
||||||
|
|
||||||
|
const { views } = story;
|
||||||
|
|
||||||
|
return updatePeerStory(global, peerId, storyId, {
|
||||||
|
views: {
|
||||||
|
...views,
|
||||||
|
...viewsUpdate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updatePeerPinnedStory<T extends GlobalState>(
|
export function updatePeerPinnedStory<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
peerId: string,
|
peerId: string,
|
||||||
|
|||||||
@ -37,6 +37,8 @@ export function selectRightColumnContentKey<T extends GlobalState>(
|
|||||||
RightColumnContent.Management
|
RightColumnContent.Management
|
||||||
) : tabState.isStatisticsShown && tabState.statistics.currentMessageId ? (
|
) : tabState.isStatisticsShown && tabState.statistics.currentMessageId ? (
|
||||||
RightColumnContent.MessageStatistics
|
RightColumnContent.MessageStatistics
|
||||||
|
) : tabState.isStatisticsShown && tabState.statistics.currentStoryId ? (
|
||||||
|
RightColumnContent.StoryStatistics
|
||||||
) : selectIsStatisticsShown(global, tabId) ? (
|
) : selectIsStatisticsShown(global, tabId) ? (
|
||||||
RightColumnContent.Statistics
|
RightColumnContent.Statistics
|
||||||
) : tabState.boostStatistics ? (
|
) : tabState.boostStatistics ? (
|
||||||
|
|||||||
@ -33,7 +33,6 @@ import type {
|
|||||||
ApiKeyboardButton,
|
ApiKeyboardButton,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
ApiMessageStatistics,
|
|
||||||
ApiMyBoost,
|
ApiMyBoost,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
ApiNotification,
|
ApiNotification,
|
||||||
@ -44,6 +43,7 @@ import type {
|
|||||||
ApiPeerStories,
|
ApiPeerStories,
|
||||||
ApiPhoneCall,
|
ApiPhoneCall,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
|
ApiPostStatistics,
|
||||||
ApiPremiumPromo,
|
ApiPremiumPromo,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiReceipt,
|
ApiReceipt,
|
||||||
@ -499,8 +499,10 @@ export type TabState = {
|
|||||||
|
|
||||||
statistics: {
|
statistics: {
|
||||||
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
|
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
|
||||||
currentMessage?: ApiMessageStatistics;
|
currentMessage?: ApiPostStatistics;
|
||||||
currentMessageId?: number;
|
currentMessageId?: number;
|
||||||
|
currentStory?: ApiPostStatistics;
|
||||||
|
currentStoryId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
newContact?: {
|
newContact?: {
|
||||||
@ -1495,6 +1497,9 @@ export interface ActionPayloads {
|
|||||||
toggleMessageStatistics: ({
|
toggleMessageStatistics: ({
|
||||||
messageId?: number;
|
messageId?: number;
|
||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
|
toggleStoryStatistics: ({
|
||||||
|
storyId?: number;
|
||||||
|
} & WithTabId) | undefined;
|
||||||
loadStatistics: {
|
loadStatistics: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
@ -1503,6 +1508,18 @@ export interface ActionPayloads {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
loadMessagePublicForwards: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
} & WithTabId;
|
||||||
|
loadStoryStatistics: {
|
||||||
|
chatId: string;
|
||||||
|
storyId: number;
|
||||||
|
} & WithTabId;
|
||||||
|
loadStoryPublicForwards: {
|
||||||
|
chatId: string;
|
||||||
|
storyId: number;
|
||||||
|
} & WithTabId;
|
||||||
loadStatisticsAsyncGraph: {
|
loadStatisticsAsyncGraph: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
token: string;
|
token: string;
|
||||||
|
|||||||
@ -1482,6 +1482,8 @@ 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.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||||
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
|
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
|
||||||
|
stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats;
|
||||||
|
stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards;
|
||||||
chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;
|
chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;
|
||||||
chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;
|
chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;
|
||||||
chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;
|
chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;
|
||||||
|
|||||||
@ -256,8 +256,10 @@
|
|||||||
"help.getPeerColors",
|
"help.getPeerColors",
|
||||||
"stats.getBroadcastStats",
|
"stats.getBroadcastStats",
|
||||||
"stats.getMegagroupStats",
|
"stats.getMegagroupStats",
|
||||||
"stats.getMessageStats",
|
|
||||||
"stats.getMessagePublicForwards",
|
"stats.getMessagePublicForwards",
|
||||||
|
"stats.getMessageStats",
|
||||||
|
"stats.getStoryStats",
|
||||||
|
"stats.getStoryPublicForwards",
|
||||||
"stats.loadAsyncGraph",
|
"stats.loadAsyncGraph",
|
||||||
"messages.getAttachMenuBots",
|
"messages.getAttachMenuBots",
|
||||||
"messages.getAttachMenuBot",
|
"messages.getAttachMenuBot",
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export function createProjection(params) {
|
|||||||
if (end === 1) {
|
if (end === 1) {
|
||||||
effectiveWidth -= xPadding;
|
effectiveWidth -= xPadding;
|
||||||
}
|
}
|
||||||
const xFactor = effectiveWidth / ((end - begin) * totalXWidth);
|
const xFactor = effectiveWidth / ((end !== begin ? end - begin : 1) * totalXWidth);
|
||||||
let xOffsetPx = (begin * totalXWidth) * xFactor;
|
let xOffsetPx = (begin * totalXWidth) * xFactor;
|
||||||
if (begin === 0) {
|
if (begin === 0) {
|
||||||
xOffsetPx -= xPadding;
|
xOffsetPx -= xPadding;
|
||||||
|
|||||||
@ -277,6 +277,7 @@ export enum RightColumnContent {
|
|||||||
Statistics,
|
Statistics,
|
||||||
BoostStatistics,
|
BoostStatistics,
|
||||||
MessageStatistics,
|
MessageStatistics,
|
||||||
|
StoryStatistics,
|
||||||
StickerSearch,
|
StickerSearch,
|
||||||
GifSearch,
|
GifSearch,
|
||||||
PollResults,
|
PollResults,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user