Statistics: Story Stats for Channels (#4097)
This commit is contained in:
parent
74fd74203e
commit
78dac9bbaf
@ -204,8 +204,8 @@ export function buildApiMessageWithChatId(
|
||||
content,
|
||||
date: mtpMessage.date,
|
||||
senderId: fromId || (mtpMessage.out && mtpMessage.post && currentUserId) || chatId,
|
||||
views: mtpMessage.views,
|
||||
forwards: mtpMessage.forwards,
|
||||
viewsCount: mtpMessage.views,
|
||||
forwardsCount: mtpMessage.forwards,
|
||||
isScheduled,
|
||||
isFromScheduled: mtpMessage.fromScheduled,
|
||||
isSilent: mtpMessage.silent,
|
||||
|
||||
@ -4,16 +4,19 @@ import type {
|
||||
ApiChannelStatistics,
|
||||
ApiGroupStatistics,
|
||||
ApiMessagePublicForward,
|
||||
ApiMessageStatistics,
|
||||
ApiPostStatistics,
|
||||
ApiStoryPublicForward,
|
||||
StatisticsGraph,
|
||||
StatisticsMessageInteractionCounter,
|
||||
StatisticsOverviewItem,
|
||||
StatisticsOverviewPercentage,
|
||||
StatisticsOverviewPeriod,
|
||||
StatisticsStoryInteractionCounter,
|
||||
} from '../../types';
|
||||
|
||||
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 {
|
||||
return {
|
||||
@ -28,26 +31,43 @@ export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiC
|
||||
viewsBySourceGraph: (stats.viewsBySourceGraph as GramJs.StatsGraphAsync).token,
|
||||
newFollowersBySourceGraph: (stats.newFollowersBySourceGraph as GramJs.StatsGraphAsync).token,
|
||||
interactionsGraph: (stats.interactionsGraph as GramJs.StatsGraphAsync).token,
|
||||
reactionsByEmotionGraph: (stats.reactionsByEmotionGraph as GramJs.StatsGraphAsync).token,
|
||||
storyInteractionsGraph: (stats.storyInteractionsGraph as GramJs.StatsGraphAsync).token,
|
||||
storyReactionsByEmotionGraph: (stats.storyReactionsByEmotionGraph as GramJs.StatsGraphAsync).token,
|
||||
|
||||
// Statistics overview
|
||||
followers: buildStatisticsOverview(stats.followers),
|
||||
viewsPerPost: buildStatisticsOverview(stats.viewsPerPost),
|
||||
sharesPerPost: buildStatisticsOverview(stats.sharesPerPost),
|
||||
enabledNotifications: buildStatisticsPercentage(stats.enabledNotifications),
|
||||
reactionsPerPost: buildStatisticsOverview(stats.reactionsPerPost),
|
||||
viewsPerStory: buildStatisticsOverview(stats.viewsPerStory),
|
||||
sharesPerStory: buildStatisticsOverview(stats.sharesPerStory),
|
||||
reactionsPerStory: buildStatisticsOverview(stats.reactionsPerStory),
|
||||
|
||||
// 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,
|
||||
): StatisticsMessageInteractionCounter | undefined {
|
||||
): StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter | undefined {
|
||||
if (interaction instanceof GramJs.PostInteractionCountersMessage) {
|
||||
return {
|
||||
msgId: interaction.msgId,
|
||||
forwards: interaction.forwards,
|
||||
views: interaction.views,
|
||||
forwardsCount: interaction.forwards,
|
||||
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 {
|
||||
viewsGraph: buildGraph(stats.viewsGraph),
|
||||
reactionsGraph: buildGraph(stats.reactionsByEmotionGraph),
|
||||
};
|
||||
}
|
||||
|
||||
@ -88,22 +109,30 @@ export function buildMessagePublicForwards(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.messages.map((message) => {
|
||||
const peerId = buildApiPeerId((message.peerId as GramJs.PeerChannel).channelId, 'channel');
|
||||
const channel = result.chats.find((p) => buildApiPeerId(p.id, 'channel') === peerId);
|
||||
return result.messages.map((message) => buildApiMessagePublicForward(message, result.chats));
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
username: (channel as GramJs.Channel).username,
|
||||
avatarHash: buildAvatarHash((channel as GramJs.Channel).photo),
|
||||
},
|
||||
};
|
||||
peerId,
|
||||
storyId: story.id,
|
||||
viewsCount: (story as GramJs.StoryItem).views?.viewsCount || 0,
|
||||
reactionsCount: (story as GramJs.StoryItem).views?.reactionsCount || 0,
|
||||
} as ApiStoryPublicForward;
|
||||
});
|
||||
}
|
||||
|
||||
@ -191,3 +220,23 @@ function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewP
|
||||
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,
|
||||
ApiStealthMode,
|
||||
ApiStoryForwardInfo,
|
||||
ApiStoryView,
|
||||
ApiStoryView, ApiStoryViews,
|
||||
ApiTypeStory,
|
||||
MediaContent,
|
||||
} from '../../types';
|
||||
@ -67,13 +67,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
||||
...(selectedContacts && { isForSelectedContacts: true }),
|
||||
...(closeFriends && { isForCloseFriends: true }),
|
||||
...(noforwards && { noForwards: true }),
|
||||
...(views?.viewsCount && { viewsCount: views.viewsCount }),
|
||||
...(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')),
|
||||
}),
|
||||
...(views && { views: buildApiStoryViews(views) }),
|
||||
...(out && { isOut: true }),
|
||||
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
||||
...(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 {
|
||||
const {
|
||||
userId, date, reaction, blockedMyStoriesFrom, blocked,
|
||||
|
||||
@ -94,7 +94,7 @@ export {
|
||||
|
||||
export {
|
||||
fetchChannelStatistics, fetchGroupStatistics, fetchMessageStatistics,
|
||||
fetchMessagePublicForwards, fetchStatisticsAsyncGraph,
|
||||
fetchMessagePublicForwards, fetchStatisticsAsyncGraph, fetchStoryStatistics, fetchStoryPublicForwards,
|
||||
} from './statistics';
|
||||
|
||||
export {
|
||||
|
||||
@ -2,15 +2,21 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiMessagePublicForward, ApiMessageStatistics, StatisticsGraph,
|
||||
ApiChat, ApiMessagePublicForward, ApiPostStatistics, ApiStoryPublicForward, ApiUser, StatisticsGraph,
|
||||
} from '../../types';
|
||||
|
||||
import { STATISTICS_PUBLIC_FORWARDS_LIMIT } from '../../../config';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import {
|
||||
buildChannelStatistics, buildGraph,
|
||||
buildGroupStatistics, buildMessagePublicForwards, buildMessageStatistics,
|
||||
buildChannelStatistics,
|
||||
buildGraph,
|
||||
buildGroupStatistics,
|
||||
buildMessagePublicForwards,
|
||||
buildPostsStatistics,
|
||||
buildStoryPublicForwards,
|
||||
} from '../apiBuilders/statistics';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import { buildInputEntity } from '../gramjsBuilders';
|
||||
import { buildInputEntity, buildInputPeer } from '../gramjsBuilders';
|
||||
import { addEntitiesToLocalDb } from '../helpers';
|
||||
import { invokeRequest } from './client';
|
||||
|
||||
@ -62,7 +68,7 @@ export async function fetchMessageStatistics({
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
dcId?: number;
|
||||
}): Promise<ApiMessageStatistics | undefined> {
|
||||
}): Promise<ApiPostStatistics | undefined> {
|
||||
const result = await invokeRequest(new GramJs.stats.GetMessageStats({
|
||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||
msgId: messageId,
|
||||
@ -74,22 +80,30 @@ export async function fetchMessageStatistics({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildMessageStatistics(result);
|
||||
return buildPostsStatistics(result);
|
||||
}
|
||||
|
||||
export async function fetchMessagePublicForwards({
|
||||
chat,
|
||||
messageId,
|
||||
dcId,
|
||||
offsetRate = 0,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
dcId?: number;
|
||||
}): Promise<ApiMessagePublicForward[] | undefined> {
|
||||
offsetRate?: number;
|
||||
}): Promise<{
|
||||
forwards?: ApiMessagePublicForward[];
|
||||
count?: number;
|
||||
nextRate?: number;
|
||||
} | undefined> {
|
||||
const result = await invokeRequest(new GramJs.stats.GetMessagePublicForwards({
|
||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||
msgId: messageId,
|
||||
offsetPeer: new GramJs.InputPeerEmpty(),
|
||||
offsetRate,
|
||||
limit: STATISTICS_PUBLIC_FORWARDS_LIMIT,
|
||||
}), {
|
||||
dcId,
|
||||
});
|
||||
@ -102,7 +116,13 @@ export async function fetchMessagePublicForwards({
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
}
|
||||
|
||||
return buildMessagePublicForwards(result);
|
||||
return {
|
||||
forwards: buildMessagePublicForwards(result),
|
||||
...('nextRate' in result ? {
|
||||
count: result.count,
|
||||
nextRate: result.nextRate,
|
||||
} : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchStatisticsAsyncGraph({
|
||||
@ -129,3 +149,68 @@ export async function fetchStatisticsAsyncGraph({
|
||||
|
||||
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',
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
id: update.id,
|
||||
message: { views: update.views },
|
||||
message: { viewsCount: update.views },
|
||||
});
|
||||
|
||||
// Chats
|
||||
|
||||
@ -472,8 +472,8 @@ export interface ApiMessage {
|
||||
forwardInfo?: ApiMessageForwardInfo;
|
||||
isDeleting?: boolean;
|
||||
previousLocalId?: number;
|
||||
views?: number;
|
||||
forwards?: number;
|
||||
viewsCount?: number;
|
||||
forwardsCount?: number;
|
||||
isEdited?: boolean;
|
||||
editDate?: number;
|
||||
isMentioned?: boolean;
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { ApiChat } from './chats';
|
||||
import type { ApiMessage } from './messages';
|
||||
|
||||
export interface ApiChannelStatistics {
|
||||
growthGraph?: StatisticsGraph | string;
|
||||
followersGraph?: StatisticsGraph | string;
|
||||
muteGraph?: StatisticsGraph | string;
|
||||
topHoursGraph?: StatisticsGraph | string;
|
||||
reactionsByEmotionGraph?: StatisticsGraph | string;
|
||||
storyInteractionsGraph?: StatisticsGraph | string;
|
||||
storyReactionsByEmotionGraph?: StatisticsGraph | string;
|
||||
interactionsGraph: StatisticsGraph | string;
|
||||
viewsBySourceGraph: StatisticsGraph | string;
|
||||
newFollowersBySourceGraph: StatisticsGraph | string;
|
||||
@ -14,7 +16,11 @@ export interface ApiChannelStatistics {
|
||||
viewsPerPost: StatisticsOverviewItem;
|
||||
sharesPerPost: StatisticsOverviewItem;
|
||||
enabledNotifications: StatisticsOverviewPercentage;
|
||||
recentTopMessages: Array<StatisticsMessageInteractionCounter | StatisticsMessageInteractionCounter & ApiMessage>;
|
||||
reactionsPerPost: StatisticsOverviewItem;
|
||||
viewsPerStory: StatisticsOverviewItem;
|
||||
sharesPerStory: StatisticsOverviewItem;
|
||||
reactionsPerStory: StatisticsOverviewItem;
|
||||
recentPosts: Array<StatisticsMessageInteractionCounter | StatisticsStoryInteractionCounter>;
|
||||
}
|
||||
|
||||
export interface ApiGroupStatistics {
|
||||
@ -31,12 +37,17 @@ export interface ApiGroupStatistics {
|
||||
posters: StatisticsOverviewItem;
|
||||
}
|
||||
|
||||
export interface ApiMessageStatistics {
|
||||
export interface ApiPostStatistics {
|
||||
viewsGraph?: StatisticsGraph | string;
|
||||
forwards?: number;
|
||||
views?: number;
|
||||
reactionsGraph?: StatisticsGraph | string;
|
||||
forwardsCount?: number;
|
||||
viewsCount?: number;
|
||||
reactionsCount?: number;
|
||||
publicForwards?: number;
|
||||
publicForwardsData?: ApiMessagePublicForward[];
|
||||
publicForwardsData?: (ApiMessagePublicForward | ApiStoryPublicForward)[];
|
||||
|
||||
nextRate?: number;
|
||||
nextOffsetId?: string;
|
||||
}
|
||||
|
||||
export interface ApiBoostStatistics {
|
||||
@ -53,6 +64,13 @@ export interface ApiMessagePublicForward {
|
||||
chat: ApiChat;
|
||||
}
|
||||
|
||||
export interface ApiStoryPublicForward {
|
||||
peerId: string;
|
||||
storyId: number;
|
||||
viewsCount?: number;
|
||||
reactionsCount?: number;
|
||||
}
|
||||
|
||||
export interface StatisticsGraph {
|
||||
type: string;
|
||||
zoomToken?: string;
|
||||
@ -95,6 +113,14 @@ export interface StatisticsOverviewPeriod {
|
||||
|
||||
export interface StatisticsMessageInteractionCounter {
|
||||
msgId: number;
|
||||
forwards: number;
|
||||
views: number;
|
||||
forwardsCount: 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;
|
||||
isOut?: true;
|
||||
noForwards?: boolean;
|
||||
views?: ApiStoryViews;
|
||||
visibility?: ApiPrivacySettings;
|
||||
sentReaction?: ApiReaction;
|
||||
mediaAreas?: ApiMediaArea[];
|
||||
forwardInfo?: ApiStoryForwardInfo;
|
||||
}
|
||||
|
||||
export interface ApiStoryViews {
|
||||
viewsCount?: number;
|
||||
forwardsCount?: number;
|
||||
reactionsCount?: number;
|
||||
reactions?: ApiReactionCount[];
|
||||
recentViewerIds?: string[];
|
||||
visibility?: ApiPrivacySettings;
|
||||
sentReaction?: ApiReaction;
|
||||
mediaAreas?: ApiMediaArea[];
|
||||
forwardInfo?: ApiStoryForwardInfo;
|
||||
}
|
||||
|
||||
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 Statistics } from '../components/right/statistics/Statistics';
|
||||
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 CreateTopic } from '../components/right/CreateTopic';
|
||||
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 {
|
||||
bottom: -0.125rem;
|
||||
right: -0.125rem;
|
||||
|
||||
@ -56,6 +56,7 @@ type OwnProps = {
|
||||
forPremiumPromo?: boolean;
|
||||
withStoryGap?: boolean;
|
||||
withStorySolid?: boolean;
|
||||
forceUnreadStorySolid?: boolean;
|
||||
storyViewerOrigin?: StoryViewerOrigin;
|
||||
storyViewerMode?: 'full' | 'single-peer' | 'disabled';
|
||||
loopIndefinitely?: boolean;
|
||||
@ -76,6 +77,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
forPremiumPromo,
|
||||
withStoryGap,
|
||||
withStorySolid,
|
||||
forceUnreadStorySolid,
|
||||
storyViewerOrigin,
|
||||
storyViewerMode = 'single-peer',
|
||||
loopIndefinitely,
|
||||
@ -217,7 +219,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
isForum && 'forum',
|
||||
((withStory && peer?.hasStories) || forPremiumPromo) && 'with-story-circle',
|
||||
withStorySolid && peer?.hasStories && 'with-story-solid',
|
||||
withStorySolid && peer?.hasUnreadStories && 'has-unread-story',
|
||||
withStorySolid && (peer?.hasUnreadStories || forceUnreadStorySolid) && 'has-unread-story',
|
||||
onClick && 'interactive',
|
||||
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
||||
);
|
||||
|
||||
@ -295,7 +295,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
const global = getGlobal();
|
||||
const ids = messageIds.filter((id) => selectThreadInfo(global, chatId, id)?.isCommentsInfo
|
||||
|| messagesById[id]?.views !== undefined);
|
||||
|| messagesById[id]?.viewsCount !== undefined);
|
||||
|
||||
if (!ids.length) return;
|
||||
|
||||
|
||||
@ -613,7 +613,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isLastInDocumentGroup && 'last-in-document-group',
|
||||
isLastInList && 'last-in-list',
|
||||
isOwn && 'own',
|
||||
Boolean(message.views) && 'has-views',
|
||||
Boolean(message.viewsCount) && 'has-views',
|
||||
message.isEdited && 'was-edited',
|
||||
hasMessageReply && 'has-reply',
|
||||
isContextMenuOpen && 'has-menu-open',
|
||||
@ -1338,7 +1338,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
data-has-unread-mention={message.hasUnreadMention || undefined}
|
||||
data-has-unread-reaction={hasUnreadReaction || undefined}
|
||||
data-is-pinned={isPinned || undefined}
|
||||
data-should-update-views={message.views !== undefined}
|
||||
data-should-update-views={message.viewsCount !== undefined}
|
||||
/>
|
||||
{!isInDocumentGroup && (
|
||||
<div className="message-select-control">
|
||||
|
||||
@ -105,10 +105,10 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
{isTranslated && (
|
||||
<i className="icon icon-language message-translated" onClick={onTranslationClick} />
|
||||
)}
|
||||
{Boolean(message.views) && (
|
||||
{Boolean(message.viewsCount) && (
|
||||
<>
|
||||
<span className="message-views">
|
||||
{formatIntegerCompact(message.views!)}
|
||||
{formatIntegerCompact(message.viewsCount!)}
|
||||
</span>
|
||||
<i className="icon icon-channelviews" />
|
||||
</>
|
||||
|
||||
@ -33,6 +33,7 @@ import RightSearch from './RightSearch.async';
|
||||
import BoostStatistics from './statistics/BoostStatistics';
|
||||
import MessageStatistics from './statistics/MessageStatistics.async';
|
||||
import Statistics from './statistics/Statistics.async';
|
||||
import StoryStatistics from './statistics/StoryStatistics.async';
|
||||
import StickerSearch from './StickerSearch.async';
|
||||
|
||||
import './RightColumn.scss';
|
||||
@ -86,6 +87,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
setEditingExportedInvite,
|
||||
toggleStatistics,
|
||||
toggleMessageStatistics,
|
||||
toggleStoryStatistics,
|
||||
setOpenedInviteInfo,
|
||||
requestNextManagementScreen,
|
||||
resetNextProfileTab,
|
||||
@ -107,6 +109,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
const isManagement = contentKey === RightColumnContent.Management;
|
||||
const isStatistics = contentKey === RightColumnContent.Statistics;
|
||||
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
|
||||
const isStoryStatistics = contentKey === RightColumnContent.StoryStatistics;
|
||||
const isBoostStatistics = contentKey === RightColumnContent.BoostStatistics;
|
||||
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
||||
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
||||
@ -176,6 +179,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
case RightColumnContent.MessageStatistics:
|
||||
toggleMessageStatistics();
|
||||
break;
|
||||
case RightColumnContent.StoryStatistics:
|
||||
toggleStoryStatistics();
|
||||
break;
|
||||
case RightColumnContent.Statistics:
|
||||
toggleStatistics();
|
||||
break;
|
||||
@ -322,6 +328,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
return <BoostStatistics />;
|
||||
case RightColumnContent.MessageStatistics:
|
||||
return <MessageStatistics chatId={chatId!} isActive={isOpen && isActive} />;
|
||||
case RightColumnContent.StoryStatistics:
|
||||
return <StoryStatistics chatId={chatId!} isActive={isOpen && isActive} />;
|
||||
case RightColumnContent.StickerSearch:
|
||||
return <StickerSearch onClose={close} isActive={isOpen && isActive} />;
|
||||
case RightColumnContent.GifSearch:
|
||||
@ -356,6 +364,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
isStatistics={isStatistics}
|
||||
isBoostStatistics={isBoostStatistics}
|
||||
isMessageStatistics={isMessageStatistics}
|
||||
isStoryStatistics={isStoryStatistics}
|
||||
isStickerSearch={isStickerSearch}
|
||||
isGifSearch={isGifSearch}
|
||||
isPollResults={isPollResults}
|
||||
@ -373,7 +382,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
|
||||
shouldCleanup
|
||||
cleanupExceptionKey={
|
||||
renderingContentKey === RightColumnContent.MessageStatistics
|
||||
(renderingContentKey === RightColumnContent.MessageStatistics
|
||||
|| renderingContentKey === RightColumnContent.StoryStatistics)
|
||||
? RightColumnContent.Statistics : undefined
|
||||
}
|
||||
>
|
||||
|
||||
@ -48,6 +48,7 @@ type OwnProps = {
|
||||
isStatistics?: boolean;
|
||||
isBoostStatistics?: boolean;
|
||||
isMessageStatistics?: boolean;
|
||||
isStoryStatistics?: boolean;
|
||||
isStickerSearch?: boolean;
|
||||
isGifSearch?: boolean;
|
||||
isPollResults?: boolean;
|
||||
@ -89,6 +90,7 @@ enum HeaderContent {
|
||||
Search,
|
||||
Statistics,
|
||||
MessageStatistics,
|
||||
StoryStatistics,
|
||||
BoostStatistics,
|
||||
Management,
|
||||
ManageInitial,
|
||||
@ -128,6 +130,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
isManagement,
|
||||
isStatistics,
|
||||
isMessageStatistics,
|
||||
isStoryStatistics,
|
||||
isBoostStatistics,
|
||||
isStickerSearch,
|
||||
isGifSearch,
|
||||
@ -291,6 +294,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
HeaderContent.Statistics
|
||||
) : isMessageStatistics ? (
|
||||
HeaderContent.MessageStatistics
|
||||
) : isStoryStatistics ? (
|
||||
HeaderContent.StoryStatistics
|
||||
) : isBoostStatistics ? (
|
||||
HeaderContent.BoostStatistics
|
||||
) : isCreatingTopic ? (
|
||||
@ -442,6 +447,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
|
||||
case HeaderContent.MessageStatistics:
|
||||
return <h3>{lang('Stats.MessageTitle')}</h3>;
|
||||
case HeaderContent.StoryStatistics:
|
||||
return <h3>{lang('Stats.StoryTitle')}</h3>;
|
||||
case HeaderContent.BoostStatistics:
|
||||
return <h3>{lang('Boosts')}</h3>;
|
||||
case HeaderContent.SharedMedia:
|
||||
@ -522,6 +529,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
|| contentKey === HeaderContent.StoryList
|
||||
|| contentKey === HeaderContent.AddingMembers
|
||||
|| contentKey === HeaderContent.MessageStatistics
|
||||
|| contentKey === HeaderContent.StoryStatistics
|
||||
|| isManagement
|
||||
);
|
||||
|
||||
|
||||
@ -1,24 +1,30 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef,
|
||||
useState,
|
||||
memo, useEffect, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
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 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 StatisticsPublicForward from './StatisticsPublicForward';
|
||||
|
||||
import './Statistics.scss';
|
||||
import styles from './Statistics.module.scss';
|
||||
|
||||
type ILovelyChart = { create: Function };
|
||||
let lovelyChartPromise: Promise<ILovelyChart>;
|
||||
@ -35,8 +41,9 @@ async function ensureLovelyChart() {
|
||||
|
||||
const GRAPH_TITLES = {
|
||||
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 = {
|
||||
chatId: string;
|
||||
@ -44,25 +51,25 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
statistics?: ApiMessageStatistics;
|
||||
statistics?: ApiPostStatistics;
|
||||
messageId?: number;
|
||||
dcId?: number;
|
||||
};
|
||||
|
||||
const Statistics: FC<OwnProps & StateProps> = ({
|
||||
function Statistics({
|
||||
chatId,
|
||||
isActive,
|
||||
statistics,
|
||||
dcId,
|
||||
messageId,
|
||||
}) => {
|
||||
}: 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 { loadMessageStatistics, loadStatisticsAsyncGraph } = getActions();
|
||||
const { loadMessageStatistics, loadMessagePublicForwards, loadStatisticsAsyncGraph } = getActions();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
useEffect(() => {
|
||||
@ -144,34 +151,48 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
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) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
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')} />
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
|
||||
<div ref={containerRef}>
|
||||
{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>
|
||||
|
||||
{Boolean(statistics.publicForwards) && (
|
||||
<div className="Statistics__public-forwards">
|
||||
<h2 className="Statistics__public-forwards-title">{lang('Stats.Message.PublicShares')}</h2>
|
||||
<div className={styles.publicForwards}>
|
||||
<h2 className={styles.publicForwardsTitle}>{lang('Stats.Message.PublicShares')}</h2>
|
||||
|
||||
{statistics.publicForwardsData!.map((item: ApiMessagePublicForward) => (
|
||||
<StatisticsPublicForward data={item} />
|
||||
))}
|
||||
<InfiniteScroll
|
||||
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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(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 {
|
||||
ApiChannelStatistics,
|
||||
ApiChat,
|
||||
ApiGroupStatistics,
|
||||
ApiMessage,
|
||||
ApiTypeStory,
|
||||
StatisticsGraph,
|
||||
StatisticsMessageInteractionCounter,
|
||||
} 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 { callApi } from '../../../api/gramjs';
|
||||
|
||||
@ -23,8 +30,9 @@ import useLang from '../../../hooks/useLang';
|
||||
import Loading from '../../ui/Loading';
|
||||
import StatisticsOverview from './StatisticsOverview';
|
||||
import StatisticsRecentMessage from './StatisticsRecentMessage';
|
||||
import StatisticsRecentStory from './StatisticsRecentStory';
|
||||
|
||||
import './Statistics.scss';
|
||||
import styles from './Statistics.module.scss';
|
||||
|
||||
type ILovelyChart = { create: Function };
|
||||
let lovelyChartPromise: Promise<ILovelyChart>;
|
||||
@ -48,6 +56,9 @@ const CHANNEL_GRAPHS_TITLES = {
|
||||
newFollowersBySourceGraph: 'ChannelStats.Graph.NewFollowersBySource',
|
||||
languagesGraph: 'ChannelStats.Graph.Language',
|
||||
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)[];
|
||||
|
||||
@ -66,16 +77,22 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
export type StateProps = {
|
||||
chat?: ApiChat;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics;
|
||||
dcId?: number;
|
||||
isGroup: boolean;
|
||||
messagesById: Record<string, ApiMessage>;
|
||||
storiesById?: Record<string, ApiTypeStory>;
|
||||
};
|
||||
|
||||
const Statistics: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
chat,
|
||||
statistics,
|
||||
dcId,
|
||||
isGroup,
|
||||
messagesById,
|
||||
storiesById,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -163,7 +180,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
loadedCharts.current.push(name);
|
||||
|
||||
containerRef.current!.children[index].classList.remove('hidden');
|
||||
containerRef.current!.children[index].classList.remove(styles.hidden);
|
||||
});
|
||||
|
||||
forceUpdate();
|
||||
@ -177,7 +194,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
|
||||
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
|
||||
<StatisticsOverview
|
||||
statistics={statistics}
|
||||
type={isGroup ? 'group' : 'channel'}
|
||||
@ -188,17 +205,43 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
<div ref={containerRef}>
|
||||
{graphs.map((graph) => (
|
||||
<div key={graph} className="Statistics__graph hidden" />
|
||||
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{Boolean((statistics as ApiChannelStatistics).recentTopMessages?.length) && (
|
||||
<div className="Statistics__messages">
|
||||
<h2 className="Statistics__messages-title">{lang('ChannelStats.Recent.Header')}</h2>
|
||||
{Boolean((statistics as ApiChannelStatistics).recentPosts?.length) && (
|
||||
<div className={styles.messages}>
|
||||
<h2 className={styles.messagesTitle}>{lang('ChannelStats.Recent.Header')}</h2>
|
||||
|
||||
{(statistics as ApiChannelStatistics).recentTopMessages.map((message) => (
|
||||
<StatisticsRecentMessage message={message as ApiMessage & StatisticsMessageInteractionCounter} />
|
||||
))}
|
||||
{(statistics as ApiChannelStatistics).recentPosts.map((postStatistic) => {
|
||||
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>
|
||||
@ -211,9 +254,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = selectChat(global, chatId);
|
||||
const dcId = selectChatFullInfo(global, chatId)?.statisticsDcId;
|
||||
const isGroup = chat?.type === 'chatTypeSuperGroup';
|
||||
const messagesById = selectChatMessages(global, chatId);
|
||||
const storiesById = selectPeerStories(global, chatId)?.byId;
|
||||
|
||||
return {
|
||||
statistics, dcId, isGroup,
|
||||
statistics, dcId, isGroup, chat, messagesById, storiesById,
|
||||
};
|
||||
},
|
||||
)(Statistics));
|
||||
|
||||
@ -5,36 +5,37 @@ import { getActions } from '../../../global';
|
||||
import type { ApiMessagePublicForward } from '../../../api/types';
|
||||
|
||||
import { getMainUsername } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
|
||||
import './StatisticsPublicForward.scss';
|
||||
import styles from './StatisticsPublicForward.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
data: ApiMessagePublicForward;
|
||||
};
|
||||
|
||||
const StatisticsPublicForward: FC<OwnProps> = ({ data }) => {
|
||||
const StatisticsMessagePublicForward: FC<OwnProps> = ({ data }) => {
|
||||
const lang = useLang();
|
||||
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(() => {
|
||||
openChatByUsername({ username: username!, messageId: data.messageId });
|
||||
}, [data.messageId, openChatByUsername, username]);
|
||||
|
||||
return (
|
||||
<div className="StatisticsPublicForward" onClick={handleClick}>
|
||||
<div className={buildClassName(styles.root, 'statistic-public-forward')} onClick={handleClick}>
|
||||
<Avatar size="medium" peer={data.chat} />
|
||||
|
||||
<div className="StatisticsPublicForward__info">
|
||||
<div className="StatisticsPublicForward__title">
|
||||
<div>
|
||||
<div className={styles.title}>
|
||||
{data.title}
|
||||
</div>
|
||||
|
||||
<div className="StatisticsPublicForward__views">
|
||||
<div className={styles.views}>
|
||||
{lang('ChannelStats.ViewsCount', data.views, 'i')}
|
||||
</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 {
|
||||
ApiBoostStatistics,
|
||||
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsOverviewItem,
|
||||
ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsOverviewItem,
|
||||
} from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -12,7 +12,7 @@ import { formatInteger, formatIntegerCompact } from '../../../util/textFormat';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import './StatisticsOverview.scss';
|
||||
import styles from './StatisticsOverview.module.scss';
|
||||
|
||||
type OverviewCell = {
|
||||
name: string;
|
||||
@ -30,7 +30,15 @@ const CHANNEL_OVERVIEW: OverviewCell[][] = [
|
||||
],
|
||||
[
|
||||
{ name: 'viewsPerPost', title: 'ChannelStats.Overview.ViewsPerPost' },
|
||||
{ name: 'viewsPerStory', title: 'ChannelStats.Overview.ViewsPerStory' },
|
||||
],
|
||||
[
|
||||
{ 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[][] = [
|
||||
[
|
||||
{ name: 'views', title: 'Stats.Message.Views', isPlain: true },
|
||||
{
|
||||
name: 'forwards', title: 'Stats.Message.PrivateShares', isPlain: true, isApproximate: true,
|
||||
},
|
||||
{ name: 'viewsCount', title: 'Stats.Message.Views', isPlain: true },
|
||||
{ name: 'publicForwards', title: 'Stats.Message.PublicShares', isPlain: 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 = {
|
||||
type: StatisticsType;
|
||||
title?: string;
|
||||
className?: string;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics | ApiBoostStatistics;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiPostStatistics | ApiBoostStatistics;
|
||||
};
|
||||
|
||||
const StatisticsOverview: FC<OwnProps> = ({
|
||||
@ -99,7 +119,7 @@ const StatisticsOverview: FC<OwnProps> = ({
|
||||
const isChangeNegative = Number(change) < 0;
|
||||
|
||||
return (
|
||||
<span className={buildClassName('StatisticsOverview__value', isChangeNegative && 'negative')}>
|
||||
<span className={buildClassName(styles.value, isChangeNegative && styles.negative)}>
|
||||
{isChangeNegative ? `-${formatIntegerCompact(Math.abs(change))}` : `+${formatIntegerCompact(change)}`}
|
||||
{percentage && (
|
||||
<>
|
||||
@ -113,26 +133,25 @@ const StatisticsOverview: FC<OwnProps> = ({
|
||||
|
||||
const { period } = (statistics as ApiGroupStatistics);
|
||||
|
||||
const schema = type === 'boost' ? BOOST_OVERVIEW : type === 'message' ? MESSAGE_OVERVIEW : type === 'group'
|
||||
? GROUP_OVERVIEW : CHANNEL_OVERVIEW;
|
||||
const schema = getSchemaByType(type);
|
||||
|
||||
return (
|
||||
<div className={buildClassName('StatisticsOverview', className)}>
|
||||
<div className="StatisticsOverview__header">
|
||||
<div className={buildClassName(styles.root, className)}>
|
||||
<div className={styles.header}>
|
||||
{title && (
|
||||
<div className="StatisticsOverview__title">
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{period && (
|
||||
<div className="StatisticsOverview__caption">
|
||||
<div className={styles.caption}>
|
||||
{formatFullDate(lang, period.minDate * 1000)} — {formatFullDate(lang, period.maxDate * 1000)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<table className="StatisticsOverview__table">
|
||||
<table className={styles.table}>
|
||||
{schema.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell: OverviewCell) => {
|
||||
@ -140,39 +159,39 @@ const StatisticsOverview: FC<OwnProps> = ({
|
||||
|
||||
if (cell.isPlain) {
|
||||
return (
|
||||
<td className="StatisticsOverview__table-cell">
|
||||
<b className="StatisticsOverview__table-value">
|
||||
<td className={styles.tableCell}>
|
||||
<b className={styles.tableValue}>
|
||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
|
||||
</b>
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.isPercentage) {
|
||||
return (
|
||||
<td className="StatisticsOverview__table-cell">
|
||||
<td className={styles.tableCell}>
|
||||
{cell.withAbsoluteValue && (
|
||||
<span className="StatisticsOverview__table-value">
|
||||
<span className={styles.tableValue}>
|
||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`}
|
||||
</span>
|
||||
)}
|
||||
<span className={`StatisticsOverview__table-${cell.withAbsoluteValue ? 'secondary-' : ''}value`}>
|
||||
<span className={cell.withAbsoluteValue ? styles.tableSecondaryValue : styles.tableValue}>
|
||||
{field.percentage}%
|
||||
</span>
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<td className="StatisticsOverview__table-cell">
|
||||
<b className="StatisticsOverview__table-value">
|
||||
<td className={styles.tableCell}>
|
||||
<b className={styles.tableValue}>
|
||||
{formatIntegerCompact(field.current)}
|
||||
</b>
|
||||
{' '}
|
||||
{renderOverviewItemValue(field)}
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
<h3 className={styles.tableHeading}>{lang(cell.title)}</h3>
|
||||
</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);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.StatisticsPublicForward {
|
||||
.root {
|
||||
position: relative;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
padding: 0.5rem 0.75rem;
|
||||
@ -9,17 +9,17 @@
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
|
||||
.Avatar {
|
||||
:global(.Avatar) {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
&__views {
|
||||
color: var(--color-text-meta);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.views {
|
||||
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 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 = {
|
||||
message: ApiMessage & StatisticsMessageInteractionCounter;
|
||||
postStatistic: StatisticsMessageInteractionCounter;
|
||||
message: ApiMessage;
|
||||
};
|
||||
|
||||
const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
||||
const StatisticsRecentMessage: FC<OwnProps> = ({ postStatistic, message }) => {
|
||||
const lang = useLang();
|
||||
const { toggleMessageStatistics } = getActions();
|
||||
|
||||
@ -39,27 +43,25 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ message }) => {
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
'StatisticsRecentMessage',
|
||||
Boolean(mediaBlobUrl || mediaThumbnail) && 'StatisticsRecentMessage--with-image',
|
||||
styles.root,
|
||||
Boolean(mediaBlobUrl || mediaThumbnail) && styles.withImage,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="StatisticsRecentMessage__title">
|
||||
<div className="StatisticsRecentMessage__summary">
|
||||
<div className={styles.title}>
|
||||
<div className={styles.summary}>
|
||||
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||
</div>
|
||||
<div className="StatisticsRecentMessage__meta">
|
||||
{lang('ChannelStats.ViewsCount', message.views, 'i')}
|
||||
<div className={styles.meta}>
|
||||
{lang('ChannelStats.ViewsCount', postStatistic.viewsCount, 'i')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="StatisticsRecentMessage__info">
|
||||
<div className="StatisticsRecentMessage__date">
|
||||
<div className={styles.info}>
|
||||
<div className={styles.date}>
|
||||
{formatDateTimeToString(message.date * 1000, lang.code)}
|
||||
</div>
|
||||
<div className="StatisticsRecentMessage__meta">
|
||||
{message.forwards ? lang('ChannelStats.SharesCount', message.forwards) : 'No shares'}
|
||||
</div>
|
||||
<StatisticsRecentPostMeta postStatistic={postStatistic} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -71,14 +73,14 @@ function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRo
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="media-preview">
|
||||
<span>
|
||||
<img
|
||||
src={blobUrl}
|
||||
alt=""
|
||||
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)}
|
||||
</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 {
|
||||
viewsCount, forwardsCount, reactionsCount, isOut, peerId, id: storyId, sentReaction,
|
||||
views, isOut, peerId, id: storyId, sentReaction,
|
||||
} = story;
|
||||
const { viewsCount, forwardsCount, reactionsCount } = views || {};
|
||||
const isChannel = !isUserId(peerId);
|
||||
|
||||
const isSentStoryReactionHeart = sentReaction && 'emoticon' in sentReaction
|
||||
@ -50,11 +51,11 @@ const StoryFooter = ({
|
||||
const recentViewers = useMemo(() => {
|
||||
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;
|
||||
|
||||
return recentViewerIds.map((id) => usersById[id]).filter(Boolean);
|
||||
}, [story]);
|
||||
}, [views]);
|
||||
|
||||
const handleOpenStoryViewModal = useLastCallback(() => {
|
||||
openStoryViewModal({ storyId });
|
||||
|
||||
@ -69,8 +69,7 @@ function StoryViewModal({
|
||||
|
||||
const isOpen = Boolean(story);
|
||||
const isExpired = Boolean(story?.date) && (story!.date + viewersExpirePeriod) < getServerTime();
|
||||
const viewsCount = story?.viewsCount || 0;
|
||||
const reactionsCount = story?.reactionsCount || 0;
|
||||
const { viewsCount = 0, reactionsCount = 0 } = story?.views || {};
|
||||
|
||||
const shouldShowJustContacts = story?.isPublic && viewsCount > STORY_VIEWS_MIN_CONTACTS_FILTER;
|
||||
const shouldShowSortByReactions = reactionsCount > STORY_MIN_REACTIONS_SORT;
|
||||
|
||||
@ -44,7 +44,8 @@ const MediaAreaSuggestedReaction = ({
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
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 isChannel = !isUserId(peerId);
|
||||
|
||||
@ -90,6 +90,7 @@ export const COMMON_CHATS_LIMIT = 100;
|
||||
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
||||
export const STORY_LIST_LIMIT = 100;
|
||||
export const API_GENERAL_ID_LIMIT = 100;
|
||||
export const STATISTICS_PUBLIC_FORWARDS_LIMIT = 50;
|
||||
|
||||
export const STORY_VIEWS_MIN_SEARCH = 15;
|
||||
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'));
|
||||
result.viewsInfo.forEach((update) => {
|
||||
global = updateChatMessage(global, chatId, update.id, {
|
||||
views: update.views,
|
||||
forwards: update.forwards,
|
||||
viewsCount: update.views,
|
||||
forwardsCount: update.forwards,
|
||||
});
|
||||
|
||||
global = updateThreadInfo(global, chatId, update.id, update.threadInfo);
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
addChats,
|
||||
addUsers,
|
||||
updateMessageStatistics, updateStatistics, updateStatisticsGraph,
|
||||
updateMessageStatistics,
|
||||
updateStatistics,
|
||||
updateStatisticsGraph,
|
||||
updateStoryStatistics,
|
||||
} from '../../reducers';
|
||||
import { selectChat, selectChatFullInfo, selectChatMessages } from '../../selectors';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectChatMessages,
|
||||
selectPeerStory,
|
||||
selectTabState,
|
||||
} from '../../selectors';
|
||||
|
||||
addActionHandler('loadStatistics', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, isGroup, tabId = getCurrentTabId() } = payload;
|
||||
@ -28,13 +39,6 @@ addActionHandler('loadStatistics', async (global, actions, payload): Promise<voi
|
||||
const { stats, users } = result;
|
||||
|
||||
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);
|
||||
setGlobal(global);
|
||||
});
|
||||
@ -55,18 +59,63 @@ addActionHandler('loadMessageStatistics', async (global, actions, payload): Prom
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
const { views, forwards } = selectChatMessages(global, chatId)[messageId];
|
||||
result.views = views;
|
||||
result.forwards = forwards;
|
||||
|
||||
const publicForwards = await callApi('fetchMessagePublicForwards', { chat, messageId, dcId });
|
||||
result.publicForwards = publicForwards?.length;
|
||||
result.publicForwardsData = publicForwards;
|
||||
|
||||
global = getGlobal();
|
||||
const {
|
||||
viewsCount,
|
||||
forwardsCount,
|
||||
reactions,
|
||||
} = selectChatMessages(global, chatId)[messageId] || {};
|
||||
result.viewsCount = viewsCount;
|
||||
result.forwardsCount = forwardsCount;
|
||||
result.reactionsCount = reactions?.results
|
||||
? reactions?.results.reduce((acc, reaction) => acc + reaction.count, 0)
|
||||
: undefined;
|
||||
|
||||
global = updateMessageStatistics(global, result, tabId);
|
||||
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> => {
|
||||
@ -89,3 +138,80 @@ addActionHandler('loadStatisticsAsyncGraph', async (global, actions, payload): P
|
||||
global = updateStatisticsGraph(global, chatId, name, result, tabId);
|
||||
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,
|
||||
updatePeerStoriesHidden,
|
||||
updatePeerStory,
|
||||
updatePeerStoryViews,
|
||||
updatePeersWithStories,
|
||||
updateSentStoryReaction,
|
||||
updateStealthMode,
|
||||
@ -360,7 +361,7 @@ addActionHandler('loadStoryViews', async (global, actions, payload): Promise<voi
|
||||
|
||||
if (isPreload && result.views?.length) {
|
||||
const recentViewerIds = result.views.map((view) => view.userId);
|
||||
global = updatePeerStory(global, peerId, storyId, {
|
||||
global = updatePeerStoryViews(global, peerId, storyId, {
|
||||
recentViewerIds,
|
||||
viewsCount: result.viewsCount,
|
||||
reactionsCount: result.reactionsCount,
|
||||
|
||||
@ -162,6 +162,7 @@ addActionHandler('toggleStatistics', (global, actions, payload): ActionReturnTyp
|
||||
statistics: {
|
||||
...tabState.statistics,
|
||||
currentMessageId: undefined,
|
||||
currentStoryId: undefined,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
@ -172,6 +173,18 @@ addActionHandler('toggleMessageStatistics', (global, actions, payload): ActionRe
|
||||
statistics: {
|
||||
...selectTabState(global, tabId).statistics,
|
||||
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);
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type {
|
||||
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsGraph,
|
||||
ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsGraph,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
@ -22,13 +22,27 @@ export function updateStatistics<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function updateMessageStatistics<T extends GlobalState>(
|
||||
global: T, statistics: ApiMessageStatistics,
|
||||
global: T, statistics: ApiPostStatistics,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
return updateTabState(global, {
|
||||
statistics: {
|
||||
...selectTabState(global, tabId).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);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
ApiStoryDeleted,
|
||||
ApiStorySkipped,
|
||||
ApiStoryView,
|
||||
ApiStoryViews,
|
||||
ApiTypeStory,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
@ -317,17 +318,21 @@ export function updateSentStoryReaction<T extends GlobalState>(
|
||||
const story = selectPeerStory(global, peerId, storyId);
|
||||
if (!story || !('content' in story)) return global;
|
||||
|
||||
const reactionsCount = story.reactionsCount || 0;
|
||||
const hasReaction = story.reactions?.some((r) => r.chosenOrder !== undefined);
|
||||
const reactions = updateReactionCount(story.reactions || [], [reaction].filter(Boolean));
|
||||
const { views } = story;
|
||||
const reactionsCount = views?.reactionsCount || 0;
|
||||
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 newReactionsCount = reactionsCount + countDiff;
|
||||
|
||||
global = updatePeerStory(global, peerId, storyId, {
|
||||
sentReaction: reaction,
|
||||
reactionsCount: newReactionsCount,
|
||||
reactions,
|
||||
views: {
|
||||
...views,
|
||||
reactionsCount: newReactionsCount,
|
||||
reactions,
|
||||
},
|
||||
});
|
||||
|
||||
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>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
|
||||
@ -37,6 +37,8 @@ export function selectRightColumnContentKey<T extends GlobalState>(
|
||||
RightColumnContent.Management
|
||||
) : tabState.isStatisticsShown && tabState.statistics.currentMessageId ? (
|
||||
RightColumnContent.MessageStatistics
|
||||
) : tabState.isStatisticsShown && tabState.statistics.currentStoryId ? (
|
||||
RightColumnContent.StoryStatistics
|
||||
) : selectIsStatisticsShown(global, tabId) ? (
|
||||
RightColumnContent.Statistics
|
||||
) : tabState.boostStatistics ? (
|
||||
|
||||
@ -33,7 +33,6 @@ import type {
|
||||
ApiKeyboardButton,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiMessageStatistics,
|
||||
ApiMyBoost,
|
||||
ApiNewPoll,
|
||||
ApiNotification,
|
||||
@ -44,6 +43,7 @@ import type {
|
||||
ApiPeerStories,
|
||||
ApiPhoneCall,
|
||||
ApiPhoto,
|
||||
ApiPostStatistics,
|
||||
ApiPremiumPromo,
|
||||
ApiReaction,
|
||||
ApiReceipt,
|
||||
@ -499,8 +499,10 @@ export type TabState = {
|
||||
|
||||
statistics: {
|
||||
byChatId: Record<string, ApiChannelStatistics | ApiGroupStatistics>;
|
||||
currentMessage?: ApiMessageStatistics;
|
||||
currentMessage?: ApiPostStatistics;
|
||||
currentMessageId?: number;
|
||||
currentStory?: ApiPostStatistics;
|
||||
currentStoryId?: number;
|
||||
};
|
||||
|
||||
newContact?: {
|
||||
@ -1495,6 +1497,9 @@ export interface ActionPayloads {
|
||||
toggleMessageStatistics: ({
|
||||
messageId?: number;
|
||||
} & WithTabId) | undefined;
|
||||
toggleStoryStatistics: ({
|
||||
storyId?: number;
|
||||
} & WithTabId) | undefined;
|
||||
loadStatistics: {
|
||||
chatId: string;
|
||||
isGroup: boolean;
|
||||
@ -1503,6 +1508,18 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
} & WithTabId;
|
||||
loadMessagePublicForwards: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
} & WithTabId;
|
||||
loadStoryStatistics: {
|
||||
chatId: string;
|
||||
storyId: number;
|
||||
} & WithTabId;
|
||||
loadStoryPublicForwards: {
|
||||
chatId: string;
|
||||
storyId: number;
|
||||
} & WithTabId;
|
||||
loadStatisticsAsyncGraph: {
|
||||
chatId: 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.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.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.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;
|
||||
|
||||
@ -256,8 +256,10 @@
|
||||
"help.getPeerColors",
|
||||
"stats.getBroadcastStats",
|
||||
"stats.getMegagroupStats",
|
||||
"stats.getMessageStats",
|
||||
"stats.getMessagePublicForwards",
|
||||
"stats.getMessageStats",
|
||||
"stats.getStoryStats",
|
||||
"stats.getStoryPublicForwards",
|
||||
"stats.loadAsyncGraph",
|
||||
"messages.getAttachMenuBots",
|
||||
"messages.getAttachMenuBot",
|
||||
|
||||
@ -22,7 +22,7 @@ export function createProjection(params) {
|
||||
if (end === 1) {
|
||||
effectiveWidth -= xPadding;
|
||||
}
|
||||
const xFactor = effectiveWidth / ((end - begin) * totalXWidth);
|
||||
const xFactor = effectiveWidth / ((end !== begin ? end - begin : 1) * totalXWidth);
|
||||
let xOffsetPx = (begin * totalXWidth) * xFactor;
|
||||
if (begin === 0) {
|
||||
xOffsetPx -= xPadding;
|
||||
|
||||
@ -277,6 +277,7 @@ export enum RightColumnContent {
|
||||
Statistics,
|
||||
BoostStatistics,
|
||||
MessageStatistics,
|
||||
StoryStatistics,
|
||||
StickerSearch,
|
||||
GifSearch,
|
||||
PollResults,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user