Boosts: Support admin view (#3912)
This commit is contained in:
parent
3e59a07012
commit
dd2bd5ddfe
@ -163,6 +163,8 @@ function buildStatisticsOverview({ current, previous }: GramJs.StatsAbsValueAndP
|
||||
|
||||
export function buildStatisticsPercentage(data: GramJs.StatsPercentValue): StatisticsOverviewPercentage {
|
||||
return {
|
||||
part: data.part,
|
||||
total: data.total,
|
||||
percentage: ((data.part / data.total) * 100).toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,7 +3,12 @@ import { Api as GramJs, errors } from '../../../lib/gramjs';
|
||||
import type {
|
||||
ApiApplyBoostInfo,
|
||||
ApiBoostsStatus,
|
||||
ApiMediaArea, ApiMediaAreaCoordinates, ApiMessage, ApiStealthMode, ApiStoryView, ApiTypeStory,
|
||||
ApiMediaArea,
|
||||
ApiMediaAreaCoordinates,
|
||||
ApiMessage,
|
||||
ApiStealthMode,
|
||||
ApiStoryView,
|
||||
ApiTypeStory,
|
||||
} from '../../types';
|
||||
|
||||
import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||
@ -215,6 +220,6 @@ export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus):
|
||||
hasMyBoost: Boolean(myBoost),
|
||||
boostUrl,
|
||||
nextLevelBoosts,
|
||||
...(premiumAudience && { premiumAudience: buildStatisticsPercentage(premiumAudience) }),
|
||||
...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -494,3 +494,40 @@ export async function fetchBoostsStatus({
|
||||
|
||||
return buildApiBoostsStatus(result);
|
||||
}
|
||||
|
||||
export async function fetchBoostersList({
|
||||
chat,
|
||||
offset = '',
|
||||
limit,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
offset?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.stories.GetBoostersList({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
offset,
|
||||
limit,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
|
||||
const boosterIds = result.boosters.map((booster) => booster.userId.toString());
|
||||
const boosters = buildCollectionByCallback(result.boosters, (booster) => (
|
||||
[booster.userId.toString(), booster.expires]
|
||||
));
|
||||
|
||||
return {
|
||||
count: result.count,
|
||||
users,
|
||||
boosters,
|
||||
boosterIds,
|
||||
nextOffset: result.nextOffset,
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,6 +39,13 @@ export interface ApiMessageStatistics {
|
||||
publicForwardsData?: ApiMessagePublicForward[];
|
||||
}
|
||||
|
||||
export interface ApiBoostStatistics {
|
||||
level: number;
|
||||
boosts: number;
|
||||
premiumSubscribers: StatisticsOverviewPercentage;
|
||||
remainingBoosts: number;
|
||||
}
|
||||
|
||||
export interface ApiMessagePublicForward {
|
||||
messageId: number;
|
||||
views?: number;
|
||||
@ -76,6 +83,8 @@ export interface StatisticsOverviewItem {
|
||||
}
|
||||
|
||||
export interface StatisticsOverviewPercentage {
|
||||
part: number;
|
||||
total: number;
|
||||
percentage: string;
|
||||
}
|
||||
|
||||
|
||||
@ -137,5 +137,5 @@ export type ApiBoostsStatus = {
|
||||
nextLevelBoosts?: number;
|
||||
hasMyBoost?: boolean;
|
||||
boostUrl: string;
|
||||
premiumAudience?: StatisticsOverviewPercentage;
|
||||
premiumSubscribers?: StatisticsOverviewPercentage;
|
||||
};
|
||||
|
||||
@ -17,12 +17,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: auto;
|
||||
flex: 1 0 auto;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@ -18,15 +18,17 @@ import styles from './InviteLink.module.scss';
|
||||
type OwnProps = {
|
||||
title?: string;
|
||||
inviteLink: string;
|
||||
onRevoke?: VoidFunction;
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
onRevoke?: VoidFunction;
|
||||
};
|
||||
|
||||
const InviteLink: FC<OwnProps> = ({
|
||||
title,
|
||||
inviteLink,
|
||||
onRevoke,
|
||||
isDisabled,
|
||||
className,
|
||||
onRevoke,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const { showNotification, openChatWithDraft } = getActions();
|
||||
@ -66,8 +68,8 @@ const InviteLink: FC<OwnProps> = ({
|
||||
}, [isMobile, lang]);
|
||||
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<p className="text-muted">
|
||||
<div className={className}>
|
||||
<p className={styles.title}>
|
||||
{lang(title || 'InviteLink.InviteLink')}
|
||||
</p>
|
||||
<div className={styles.primaryLink}>
|
||||
@ -88,24 +90,13 @@ const InviteLink: FC<OwnProps> = ({
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
onClick={handleCopyPrimaryClicked}
|
||||
className={styles.button}
|
||||
size="smaller"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{lang('FolderLinkScreen.LinkActionCopy')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
className={styles.button}
|
||||
size="smaller"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{lang('FolderLinkScreen.LinkActionShare')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="smaller"
|
||||
disabled={isDisabled}
|
||||
onClick={handleShare}
|
||||
>
|
||||
{lang('FolderLinkScreen.LinkActionShare')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
@import "../../styles/mixins";
|
||||
|
||||
.container {
|
||||
background-color: var(--color-background);
|
||||
padding: 1.5rem 1.5rem 0;
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
margin-bottom: 0.625rem;
|
||||
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@ -36,16 +36,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.floating-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #7E85FF;
|
||||
.floating-badge-wrapper {
|
||||
animation: rotate-in 0.5s ease-in-out;
|
||||
border-radius: 0.5rem;
|
||||
height: 2.6875rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes rotate-in {
|
||||
@ -63,9 +59,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.floating-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #7E85FF;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floating-badge-triangle {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
bottom: -5px;
|
||||
left: calc(var(--tail-position, 0.5) * 100%);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.floating-badge-icon {
|
||||
|
||||
@ -21,6 +21,8 @@ type OwnProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PROGRESS_LOCK = 0.1;
|
||||
|
||||
const LimitPreview: FC<OwnProps> = ({
|
||||
leftText,
|
||||
rightText,
|
||||
@ -34,6 +36,8 @@ const LimitPreview: FC<OwnProps> = ({
|
||||
const hasFloatingBadge = Boolean(floatingBadgeIcon || floatingBadgeText);
|
||||
const isProgressFull = Boolean(progress) && progress > 0.99;
|
||||
|
||||
const tailPosition = progress && (progress < PROGRESS_LOCK ? 0 : progress > 1 - PROGRESS_LOCK ? 1 : 0.5);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
@ -41,18 +45,25 @@ const LimitPreview: FC<OwnProps> = ({
|
||||
hasFloatingBadge && styles.withBadge,
|
||||
className,
|
||||
)}
|
||||
style={buildStyle(progress !== undefined && `--progress: ${progress}`)}
|
||||
style={buildStyle(
|
||||
progress !== undefined && `--progress: ${progress}`,
|
||||
tailPosition !== undefined && `--tail-position: ${tailPosition}`,
|
||||
)}
|
||||
>
|
||||
{hasFloatingBadge && (
|
||||
<div className={styles.badgeContainer}>
|
||||
<div className={styles.floatingBadge}>
|
||||
{floatingBadgeIcon && <Icon name={floatingBadgeIcon} className={styles.floatingBadgeIcon} />}
|
||||
{floatingBadgeText && (
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>{floatingBadgeText}</div>
|
||||
)}
|
||||
<div className={styles.floatingBadgeWrapper}>
|
||||
<div className={styles.floatingBadge}>
|
||||
{floatingBadgeIcon && <Icon name={floatingBadgeIcon} className={styles.floatingBadgeIcon} />}
|
||||
{floatingBadgeText && (
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{floatingBadgeText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.floatingBadgeTriangle}>
|
||||
<svg width="26" height="9" viewBox="0 0 26 9" fill="none">
|
||||
<path d="M0 0H26H24.4853C22.894 0 21.3679 0.632141 20.2426 1.75736L14.4142 7.58579C13.6332 8.36684 12.3668 8.36683 11.5858 7.58579L5.75736 1.75736C4.63214 0.632139 3.10602 0 1.51472 0H0Z" fill="#7E85FF" />
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||
<path d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z" fill="#7E85FF" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,6 +43,7 @@ type OwnProps = {
|
||||
noStatusOrTyping?: boolean;
|
||||
noRtl?: boolean;
|
||||
adminMember?: ApiChatMember;
|
||||
className?: string;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
@ -75,8 +76,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
areMessagesLoaded,
|
||||
adminMember,
|
||||
ripple,
|
||||
onEmojiStatusClick,
|
||||
className,
|
||||
storyViewerOrigin,
|
||||
onEmojiStatusClick,
|
||||
}) => {
|
||||
const {
|
||||
loadFullUser,
|
||||
@ -180,7 +182,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ChatInfo" dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className={buildClassName('ChatInfo', className)} dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
|
||||
<Avatar
|
||||
key={user.id}
|
||||
size={avatarSize}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ApiBoostsStatus } from '../../../api/types';
|
||||
|
||||
export function getBoostProgressInfo(boostInfo: ApiBoostsStatus) {
|
||||
export function getBoostProgressInfo(boostInfo: ApiBoostsStatus, freezeOnLevelUp?: boolean) {
|
||||
const {
|
||||
level, boosts, currentLevelBoosts, nextLevelBoosts, hasMyBoost,
|
||||
} = boostInfo;
|
||||
@ -8,7 +8,7 @@ export function getBoostProgressInfo(boostInfo: ApiBoostsStatus) {
|
||||
const currentLevel = level;
|
||||
const hasNextLevel = Boolean(nextLevelBoosts);
|
||||
|
||||
const isJustUpgraded = boosts === currentLevelBoosts && hasMyBoost;
|
||||
const isJustUpgraded = freezeOnLevelUp && boosts === currentLevelBoosts && hasMyBoost;
|
||||
|
||||
const levelProgress = (!nextLevelBoosts || isJustUpgraded) ? 1
|
||||
: (boosts - currentLevelBoosts) / (nextLevelBoosts - currentLevelBoosts);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
#Settings {
|
||||
height: 100%;
|
||||
|
||||
@ -78,10 +80,10 @@
|
||||
align-items: center;
|
||||
padding: 0 1.5rem 1rem;
|
||||
text-align: center;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
margin-bottom: 0.625rem;
|
||||
|
||||
@include side-panel-section;
|
||||
|
||||
&.no-border {
|
||||
margin-bottom: 0;
|
||||
box-shadow: none;
|
||||
@ -107,20 +109,12 @@
|
||||
|
||||
.settings-main-menu {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@include side-panel-section;
|
||||
|
||||
> .ChatExtra {
|
||||
padding: 0 0.5rem 0.3125rem;
|
||||
margin: 0 -0.5rem 0.625rem;
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
@include side-panel-section;
|
||||
|
||||
.ListItem.narrow {
|
||||
margin-bottom: 0.25rem;
|
||||
@ -130,15 +124,8 @@
|
||||
|
||||
.settings-item-simple,
|
||||
.settings-item {
|
||||
background-color: var(--color-background);
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.SettingsGeneralBackground {
|
||||
.settings-wallpapers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
grid-gap: 0.0625rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.Loading {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.SettingsGeneralBackgroundColor {
|
||||
&:not(.is-dragging) .handle {
|
||||
transition: transform 300ms ease;
|
||||
@ -69,8 +71,7 @@
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
grid-gap: 0.0625rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.predefined-color {
|
||||
|
||||
@ -160,6 +160,7 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
|
||||
<InviteLink
|
||||
className="settings-item"
|
||||
inviteLink={!url ? lang('Loading') : url}
|
||||
onRevoke={handleRevoke}
|
||||
isDisabled={!chatsCount || isTouched}
|
||||
|
||||
@ -11,6 +11,7 @@ import { ManagementScreens } from '../../types';
|
||||
|
||||
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getHasAdminRight,
|
||||
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
@ -63,6 +64,7 @@ interface StateProps {
|
||||
canCall?: boolean;
|
||||
canMute?: boolean;
|
||||
canViewStatistics?: boolean;
|
||||
canViewBoosts?: boolean;
|
||||
canLeave?: boolean;
|
||||
canEnterVoiceChat?: boolean;
|
||||
canCreateVoiceChat?: boolean;
|
||||
@ -95,6 +97,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
canCall,
|
||||
canMute,
|
||||
canViewStatistics,
|
||||
canViewBoosts,
|
||||
canLeave,
|
||||
canEnterVoiceChat,
|
||||
canCreateVoiceChat,
|
||||
@ -425,6 +428,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
canCall={canCall}
|
||||
canMute={canMute}
|
||||
canViewStatistics={canViewStatistics}
|
||||
canViewBoosts={canViewBoosts}
|
||||
canLeave={canLeave}
|
||||
canEnterVoiceChat={canEnterVoiceChat}
|
||||
canCreateVoiceChat={canCreateVoiceChat}
|
||||
@ -486,6 +490,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && !chat.isCallActive
|
||||
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
||||
const canViewStatistics = isMainThread && chatFullInfo?.canViewStatistics;
|
||||
const canViewBoosts = isMainThread && isChannel && (canViewStatistics || getHasAdminRight(chat, 'postStories'));
|
||||
const pendingJoinRequests = isMainThread ? chatFullInfo?.requestsPending : undefined;
|
||||
const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend);
|
||||
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
||||
@ -505,6 +510,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canCall,
|
||||
canMute,
|
||||
canViewStatistics,
|
||||
canViewBoosts,
|
||||
canLeave,
|
||||
canEnterVoiceChat,
|
||||
canCreateVoiceChat,
|
||||
|
||||
@ -81,6 +81,7 @@ export type OwnProps = {
|
||||
canCall?: boolean;
|
||||
canMute?: boolean;
|
||||
canViewStatistics?: boolean;
|
||||
canViewBoosts?: boolean;
|
||||
withForumActions?: boolean;
|
||||
canLeave?: boolean;
|
||||
canEnterVoiceChat?: boolean;
|
||||
@ -137,6 +138,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canCall,
|
||||
canMute,
|
||||
canViewStatistics,
|
||||
canViewBoosts,
|
||||
pendingJoinRequests,
|
||||
canLeave,
|
||||
canEnterVoiceChat,
|
||||
@ -174,6 +176,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
openAddContactDialog,
|
||||
requestMasterAndRequestCall,
|
||||
toggleStatistics,
|
||||
openBoostStatistics,
|
||||
openGiftPremiumModal,
|
||||
openChatWithInfo,
|
||||
openCreateTopicPanel,
|
||||
@ -332,6 +335,12 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleBoostClick = useLastCallback(() => {
|
||||
openBoostStatistics({ chatId });
|
||||
setShouldCloseFast(!isRightColumnShown);
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleEnableTranslations = useLastCallback(() => {
|
||||
togglePeerTranslations({ chatId, isEnabled: true });
|
||||
closeMenu();
|
||||
@ -546,6 +555,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
{lang('ReportSelectMessages')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canViewBoosts && (
|
||||
<MenuItem
|
||||
icon="boost"
|
||||
onClick={handleBoostClick}
|
||||
>
|
||||
{lang('Boosts')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canViewStatistics && (
|
||||
<MenuItem
|
||||
icon="stats"
|
||||
|
||||
@ -114,7 +114,7 @@ const BoostModal = ({
|
||||
hasNextLevel,
|
||||
levelProgress,
|
||||
remainingBoosts,
|
||||
} = getBoostProgressInfo(info.boostStatus);
|
||||
} = getBoostProgressInfo(info.boostStatus, true);
|
||||
|
||||
const hasBoost = hasMyBoost || info.applyInfo?.type === 'already';
|
||||
const isJustUpgraded = boosts === currentLevelBoosts && hasBoost;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@import '../../styles/mixins';
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@ -17,9 +19,7 @@
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--color-background);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.general {
|
||||
|
||||
@ -18,8 +18,8 @@ import PollAnswerResults from './PollAnswerResults';
|
||||
import './PollResults.scss';
|
||||
|
||||
type OwnProps = {
|
||||
onClose: NoneToVoidFunction;
|
||||
isActive: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -28,10 +28,10 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const PollResults: FC<OwnProps & StateProps> = ({
|
||||
onClose,
|
||||
isActive,
|
||||
chat,
|
||||
message,
|
||||
onClose,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@import '../../styles/mixins';
|
||||
|
||||
.Profile {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -16,8 +18,7 @@
|
||||
|
||||
> .profile-info > .ChatExtra {
|
||||
padding: 0.875rem 0.5rem 0.5rem;
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
@include side-panel-section;
|
||||
|
||||
.narrow {
|
||||
margin-bottom: 0;
|
||||
|
||||
@ -30,6 +30,7 @@ import PollResults from './PollResults.async';
|
||||
import Profile from './Profile';
|
||||
import RightHeader from './RightHeader';
|
||||
import RightSearch from './RightSearch.async';
|
||||
import BoostStatistics from './statistics/BoostStatistics';
|
||||
import MessageStatistics from './statistics/MessageStatistics.async';
|
||||
import Statistics from './statistics/Statistics.async';
|
||||
import StickerSearch from './StickerSearch.async';
|
||||
@ -90,6 +91,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
resetNextProfileTab,
|
||||
closeCreateTopicPanel,
|
||||
closeEditTopicPanel,
|
||||
closeBoostStatistics,
|
||||
} = getActions();
|
||||
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
@ -105,6 +107,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
const isManagement = contentKey === RightColumnContent.Management;
|
||||
const isStatistics = contentKey === RightColumnContent.Statistics;
|
||||
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
|
||||
const isBoostStatistics = contentKey === RightColumnContent.BoostStatistics;
|
||||
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
||||
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
||||
const isPollResults = contentKey === RightColumnContent.PollResults;
|
||||
@ -176,6 +179,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
case RightColumnContent.Statistics:
|
||||
toggleStatistics();
|
||||
break;
|
||||
case RightColumnContent.BoostStatistics:
|
||||
closeBoostStatistics();
|
||||
break;
|
||||
case RightColumnContent.Search: {
|
||||
blurSearchInput();
|
||||
closeLocalTextSearch();
|
||||
@ -312,6 +318,8 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
|
||||
case RightColumnContent.Statistics:
|
||||
return <Statistics chatId={chatId!} />;
|
||||
case RightColumnContent.BoostStatistics:
|
||||
return <BoostStatistics />;
|
||||
case RightColumnContent.MessageStatistics:
|
||||
return <MessageStatistics chatId={chatId!} isActive={isOpen && isActive} />;
|
||||
case RightColumnContent.StickerSearch:
|
||||
@ -346,6 +354,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
||||
isSearch={isSearch}
|
||||
isManagement={isManagement}
|
||||
isStatistics={isStatistics}
|
||||
isBoostStatistics={isBoostStatistics}
|
||||
isMessageStatistics={isMessageStatistics}
|
||||
isStickerSearch={isStickerSearch}
|
||||
isGifSearch={isGifSearch}
|
||||
|
||||
@ -46,6 +46,7 @@ type OwnProps = {
|
||||
isSearch?: boolean;
|
||||
isManagement?: boolean;
|
||||
isStatistics?: boolean;
|
||||
isBoostStatistics?: boolean;
|
||||
isMessageStatistics?: boolean;
|
||||
isStickerSearch?: boolean;
|
||||
isGifSearch?: boolean;
|
||||
@ -88,6 +89,7 @@ enum HeaderContent {
|
||||
Search,
|
||||
Statistics,
|
||||
MessageStatistics,
|
||||
BoostStatistics,
|
||||
Management,
|
||||
ManageInitial,
|
||||
ManageChannelSubscribers,
|
||||
@ -126,6 +128,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
isManagement,
|
||||
isStatistics,
|
||||
isMessageStatistics,
|
||||
isBoostStatistics,
|
||||
isStickerSearch,
|
||||
isGifSearch,
|
||||
isPollResults,
|
||||
@ -139,8 +142,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
isSelf,
|
||||
canManage,
|
||||
isChannel,
|
||||
onClose,
|
||||
onScreenSelect,
|
||||
messageSearchQuery,
|
||||
stickerSearchQuery,
|
||||
gifSearchQuery,
|
||||
@ -151,6 +152,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
isBot,
|
||||
isInsideTopic,
|
||||
canEditTopic,
|
||||
onClose,
|
||||
onScreenSelect,
|
||||
}) => {
|
||||
const {
|
||||
setLocalTextSearchQuery,
|
||||
@ -288,6 +291,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
HeaderContent.Statistics
|
||||
) : isMessageStatistics ? (
|
||||
HeaderContent.MessageStatistics
|
||||
) : isBoostStatistics ? (
|
||||
HeaderContent.BoostStatistics
|
||||
) : isCreatingTopic ? (
|
||||
HeaderContent.CreateTopic
|
||||
) : isEditingTopic ? (
|
||||
@ -437,6 +442,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.BoostStatistics:
|
||||
return <h3>{lang('Boosts')}</h3>;
|
||||
case HeaderContent.SharedMedia:
|
||||
return <h3>{lang('SharedMedia')}</h3>;
|
||||
case HeaderContent.ManageChannelSubscribers:
|
||||
|
||||
@ -284,6 +284,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
{primaryInviteLink && (
|
||||
<InviteLink
|
||||
className="section"
|
||||
inviteLink={primaryInviteLink}
|
||||
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
|
||||
title={chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.Management {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@ -15,14 +17,7 @@
|
||||
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@include side-panel-section;
|
||||
|
||||
&.wide {
|
||||
padding: 1.5rem;
|
||||
@ -168,10 +163,10 @@
|
||||
|
||||
&__filter {
|
||||
padding: 0 1rem 0.25rem 0.75rem;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
margin-bottom: 0.625rem;
|
||||
|
||||
@include side-panel-section;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-shrink: 0;
|
||||
@ -341,10 +336,10 @@
|
||||
}
|
||||
|
||||
.part {
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
margin: 0 -1.5rem;
|
||||
padding: 0 1.5rem 1rem;
|
||||
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.section, .part {
|
||||
|
||||
40
src/components/right/statistics/BoostStatistics.module.scss
Normal file
40
src/components/right/statistics/BoostStatistics.module.scss
Normal file
@ -0,0 +1,40 @@
|
||||
@import '../../../styles/mixins';
|
||||
|
||||
.root {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem;
|
||||
@include side-panel-section;
|
||||
}
|
||||
|
||||
.user :global(.status) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
padding: 0.75rem 0 0 0;
|
||||
}
|
||||
|
||||
.down {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.loadMoreSpinner {
|
||||
margin-inline-end: 1rem;
|
||||
}
|
||||
154
src/components/right/statistics/BoostStatistics.tsx
Normal file
154
src/components/right/statistics/BoostStatistics.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiBoostStatistics } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateAtTime } from '../../../util/dateFormat';
|
||||
import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Icon from '../../common/Icon';
|
||||
import InviteLink from '../../common/InviteLink';
|
||||
import PremiumProgress from '../../common/PremiumProgress';
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import StatisticsOverview from './StatisticsOverview';
|
||||
|
||||
import styles from './BoostStatistics.module.scss';
|
||||
|
||||
type StateProps = {
|
||||
boostStatistics: TabState['boostStatistics'];
|
||||
};
|
||||
|
||||
const BoostStatistics = ({
|
||||
boostStatistics,
|
||||
}: StateProps) => {
|
||||
const { openChat, loadMoreBoosters, closeBoostStatistics } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const isLoaded = boostStatistics?.boostStatus;
|
||||
const status = isLoaded ? boostStatistics.boostStatus : undefined;
|
||||
|
||||
const {
|
||||
currentLevel,
|
||||
hasNextLevel,
|
||||
boosts,
|
||||
levelProgress,
|
||||
remainingBoosts,
|
||||
} = useMemo(() => {
|
||||
if (!status) {
|
||||
return {
|
||||
currentLevel: 0,
|
||||
hasNextLevel: false,
|
||||
boosts: 0,
|
||||
levelProgress: 0,
|
||||
remainingBoosts: 0,
|
||||
};
|
||||
}
|
||||
return getBoostProgressInfo(status);
|
||||
}, [status]);
|
||||
|
||||
const statsOverview = useMemo(() => {
|
||||
if (!status) return undefined;
|
||||
|
||||
return {
|
||||
level: currentLevel,
|
||||
boosts,
|
||||
premiumSubscribers: status.premiumSubscribers!,
|
||||
remainingBoosts,
|
||||
} satisfies ApiBoostStatistics;
|
||||
}, [status, boosts, currentLevel, remainingBoosts]);
|
||||
|
||||
const boostersToLoadCount = useMemo(() => {
|
||||
if (!boostStatistics?.count) return undefined;
|
||||
const loadedCount = boostStatistics.boosterIds?.length || 0;
|
||||
const totalCount = boostStatistics.count;
|
||||
return totalCount - loadedCount;
|
||||
}, [boostStatistics]);
|
||||
|
||||
const handleBoosterClick = useLastCallback((userId: string) => {
|
||||
openChat({ id: userId });
|
||||
closeBoostStatistics();
|
||||
});
|
||||
|
||||
const handleLoadMore = useLastCallback(() => {
|
||||
loadMoreBoosters();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'custom-scroll')}>
|
||||
{!isLoaded && <Loading />}
|
||||
{isLoaded && statsOverview && (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<PremiumProgress
|
||||
leftText={lang('BoostsLevel', currentLevel!)}
|
||||
rightText={hasNextLevel ? lang('BoostsLevel', currentLevel! + 1) : undefined}
|
||||
progress={levelProgress}
|
||||
floatingBadgeText={boosts.toString()}
|
||||
floatingBadgeIcon="boost"
|
||||
/>
|
||||
<StatisticsOverview className={styles.stats} statistics={statsOverview} type="boost" />
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<h4 className={styles.sectionHeader} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('Boosters')}
|
||||
</h4>
|
||||
{!boostStatistics.boosterIds?.length && (
|
||||
<div className={styles.noResults}>{lang('NoBoostersHint')}</div>
|
||||
)}
|
||||
{boostStatistics.boosterIds?.map((userId) => (
|
||||
<ListItem
|
||||
key={userId}
|
||||
className="chat-item-clickable"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleBoosterClick(userId)}
|
||||
>
|
||||
<PrivateChatInfo
|
||||
className={styles.user}
|
||||
forceShowSelf
|
||||
userId={userId}
|
||||
status={lang('BoostExpireOn', formatDateAtTime(lang, boostStatistics.boosters![userId] * 1000))}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{Boolean(boostersToLoadCount) && (
|
||||
<ListItem
|
||||
key="load-more"
|
||||
className={styles.showMore}
|
||||
disabled={boostStatistics?.isLoadingBoosters}
|
||||
onClick={handleLoadMore}
|
||||
>
|
||||
{boostStatistics?.isLoadingBoosters ? (
|
||||
<Spinner className={styles.loadMoreSpinner} />
|
||||
) : (
|
||||
<Icon name="down" className={styles.down} />
|
||||
)}
|
||||
{lang('ShowVotes', boostersToLoadCount)}
|
||||
</ListItem>
|
||||
)}
|
||||
</div>
|
||||
<InviteLink className={styles.section} inviteLink={status!.boostUrl} title={lang('LinkForBoosting')} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const boostStatistics = tabState.boostStatistics;
|
||||
|
||||
return {
|
||||
boostStatistics,
|
||||
};
|
||||
},
|
||||
)(BoostStatistics));
|
||||
@ -150,7 +150,7 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
|
||||
<StatisticsOverview statistics={statistics} isMessage />
|
||||
<StatisticsOverview statistics={statistics} type="message" title={lang('StatisticOverview')} />
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
|
||||
|
||||
@ -178,7 +178,11 @@ const Statistics: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Statistics custom-scroll', isReady && 'ready')}>
|
||||
<StatisticsOverview statistics={statistics} isGroup={isGroup} />
|
||||
<StatisticsOverview
|
||||
statistics={statistics}
|
||||
type={isGroup ? 'group' : 'channel'}
|
||||
title={lang('StatisticOverview')}
|
||||
/>
|
||||
|
||||
{!loadedCharts.current.length && <Loading />}
|
||||
|
||||
|
||||
@ -42,7 +42,13 @@
|
||||
|
||||
&-value {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
&-secondary-value {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiBoostStatistics,
|
||||
ApiChannelStatistics, ApiGroupStatistics, ApiMessageStatistics, StatisticsOverviewItem,
|
||||
} from '../../../api/types';
|
||||
|
||||
@ -17,6 +18,7 @@ type OverviewCell = {
|
||||
name: string;
|
||||
title: string;
|
||||
isPercentage?: boolean;
|
||||
withAbsoluteValue?: boolean;
|
||||
isPlain?: boolean;
|
||||
isApproximate?: boolean;
|
||||
};
|
||||
@ -55,13 +57,38 @@ const MESSAGE_OVERVIEW: OverviewCell[][] = [
|
||||
],
|
||||
];
|
||||
|
||||
const BOOST_OVERVIEW: OverviewCell[][] = [
|
||||
[
|
||||
{ name: 'level', title: 'Stats.Boosts.Level', isPlain: true },
|
||||
{
|
||||
name: 'premiumSubscribers',
|
||||
title: 'Stats.Boosts.PremiumSubscribers',
|
||||
isPercentage: true,
|
||||
isApproximate: true,
|
||||
withAbsoluteValue: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
{ name: 'boosts', title: 'Stats.Boosts.ExistingBoosts', isPlain: true },
|
||||
{ name: 'remainingBoosts', title: 'Stats.Boosts.BoostsToLevelUp', isPlain: true },
|
||||
],
|
||||
];
|
||||
|
||||
type StatisticsType = 'channel' | 'group' | 'message' | 'boost';
|
||||
|
||||
export type OwnProps = {
|
||||
isGroup?: boolean;
|
||||
isMessage?: boolean;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics;
|
||||
type: StatisticsType;
|
||||
title?: string;
|
||||
className?: string;
|
||||
statistics: ApiChannelStatistics | ApiGroupStatistics | ApiMessageStatistics | ApiBoostStatistics;
|
||||
};
|
||||
|
||||
const StatisticsOverview: FC<OwnProps> = ({ isGroup, isMessage, statistics }) => {
|
||||
const StatisticsOverview: FC<OwnProps> = ({
|
||||
title,
|
||||
type,
|
||||
statistics,
|
||||
className,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const renderOverviewItemValue = ({ change, percentage }: StatisticsOverviewItem) => {
|
||||
@ -86,10 +113,17 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, isMessage, statistics }) =>
|
||||
|
||||
const { period } = (statistics as ApiGroupStatistics);
|
||||
|
||||
const schema = type === 'boost' ? BOOST_OVERVIEW : type === 'message' ? MESSAGE_OVERVIEW : type === 'group'
|
||||
? GROUP_OVERVIEW : CHANNEL_OVERVIEW;
|
||||
|
||||
return (
|
||||
<div className="StatisticsOverview">
|
||||
<div className={buildClassName('StatisticsOverview', className)}>
|
||||
<div className="StatisticsOverview__header">
|
||||
<div className="StatisticsOverview__title">{lang('StatisticOverview')}</div>
|
||||
{title && (
|
||||
<div className="StatisticsOverview__title">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{period && (
|
||||
<div className="StatisticsOverview__caption">
|
||||
@ -99,7 +133,7 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, isMessage, statistics }) =>
|
||||
</div>
|
||||
|
||||
<table className="StatisticsOverview__table">
|
||||
{(isMessage ? MESSAGE_OVERVIEW : isGroup ? GROUP_OVERVIEW : CHANNEL_OVERVIEW).map((row) => (
|
||||
{schema.map((row) => (
|
||||
<tr>
|
||||
{row.map((cell: OverviewCell) => {
|
||||
const field = (statistics as any)[cell.name];
|
||||
@ -108,7 +142,7 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, isMessage, statistics }) =>
|
||||
return (
|
||||
<td className="StatisticsOverview__table-cell">
|
||||
<b className="StatisticsOverview__table-value">
|
||||
{cell.isApproximate ? `≈${formatInteger(field)}` : formatInteger(field)}
|
||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field)}`}
|
||||
</b>
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
</td>
|
||||
@ -118,7 +152,14 @@ const StatisticsOverview: FC<OwnProps> = ({ isGroup, isMessage, statistics }) =>
|
||||
if (cell.isPercentage) {
|
||||
return (
|
||||
<td className="StatisticsOverview__table-cell">
|
||||
<b className="StatisticsOverview__table-value">{field.percentage}%</b>
|
||||
{cell.withAbsoluteValue && (
|
||||
<span className="StatisticsOverview__table-value">
|
||||
{`${cell.isApproximate ? '≈' : ''}${formatInteger(field.part)}`}
|
||||
</span>
|
||||
)}
|
||||
<span className={`StatisticsOverview__table-${cell.withAbsoluteValue ? 'secondary-' : ''}value`}>
|
||||
{field.percentage}%
|
||||
</span>
|
||||
<h3 className="StatisticsOverview__table-heading">{lang(cell.title)}</h3>
|
||||
</td>
|
||||
);
|
||||
|
||||
@ -82,7 +82,8 @@
|
||||
|
||||
> .icon {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 2rem;
|
||||
margin-inline-start: 0.125rem;
|
||||
margin-inline-end: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@ -489,13 +490,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[dir="rtl"] {
|
||||
.ListItem-button > .icon {
|
||||
margin-left: 2rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-ellipsis {
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { DEBUG, PREVIEW_AVATAR_COUNT } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { buildCollectionByKey, unique } from '../../../util/iteratees';
|
||||
import { translate } from '../../../util/langProvider';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
@ -557,6 +557,91 @@ addActionHandler('openBoostModal', async (global, actions, payload): Promise<voi
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openBoostStatistics', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const [boostersListResult, boostStatusResult] = await Promise.all([
|
||||
callApi('fetchBoostersList', { chat }),
|
||||
callApi('fetchBoostsStatus', { chat }),
|
||||
]);
|
||||
|
||||
global = getGlobal();
|
||||
if (!boostersListResult || !boostStatusResult) {
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: undefined,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(boostersListResult.users, 'id'));
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
boostStatus: boostStatusResult,
|
||||
boosters: boostersListResult.boosters,
|
||||
boosterIds: boostersListResult.boosterIds,
|
||||
count: boostersListResult.count,
|
||||
nextOffset: boostersListResult.nextOffset,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
let tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
const chat = selectChat(global, tabState.boostStatistics.chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
...tabState.boostStatistics,
|
||||
isLoadingBoosters: true,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostersList', {
|
||||
chat,
|
||||
offset: tabState.boostStatistics.nextOffset,
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
|
||||
tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
...tabState.boostStatistics,
|
||||
boosters: {
|
||||
...tabState.boostStatistics.boosters,
|
||||
...result.boosters,
|
||||
},
|
||||
boosterIds: unique([...tabState.boostStatistics.boosterIds || [], ...result.boosterIds]),
|
||||
count: result.count,
|
||||
nextOffset: result.nextOffset,
|
||||
isLoadingBoosters: false,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
|
||||
@ -55,6 +55,7 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
global = updateTabState(global, {
|
||||
isStatisticsShown: false,
|
||||
boostStatistics: undefined,
|
||||
contentToBeScheduled: undefined,
|
||||
...(id !== selectTabState(global, tabId).forwardMessages.toChatId && {
|
||||
forwardMessages: {},
|
||||
|
||||
@ -419,3 +419,11 @@ addActionHandler('closeBoostModal', (global, actions, payload): ActionReturnType
|
||||
boostModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeBoostStatistics', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
boostStatistics: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -39,6 +39,8 @@ export function selectRightColumnContentKey<T extends GlobalState>(
|
||||
RightColumnContent.MessageStatistics
|
||||
) : selectIsStatisticsShown(global, tabId) ? (
|
||||
RightColumnContent.Statistics
|
||||
) : tabState.boostStatistics ? (
|
||||
RightColumnContent.BoostStatistics
|
||||
) : tabState.stickerSearch.query !== undefined ? (
|
||||
RightColumnContent.StickerSearch
|
||||
) : tabState.gifSearch.query !== undefined ? (
|
||||
|
||||
@ -625,6 +625,16 @@ export type TabState = {
|
||||
boostStatus?: ApiBoostsStatus;
|
||||
applyInfo?: ApiApplyBoostInfo;
|
||||
};
|
||||
|
||||
boostStatistics?: {
|
||||
chatId: string;
|
||||
boosters?: Record<string, number>;
|
||||
boosterIds?: string[];
|
||||
boostStatus?: ApiBoostsStatus;
|
||||
isLoadingBoosters?: boolean;
|
||||
nextOffset?: string;
|
||||
count?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type GlobalState = {
|
||||
@ -2084,6 +2094,11 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
} & WithTabId;
|
||||
closeBoostModal: WithTabId | undefined;
|
||||
openBoostStatistics: {
|
||||
chatId: string;
|
||||
} & WithTabId;
|
||||
closeBoostStatistics: WithTabId | undefined;
|
||||
loadMoreBoosters: WithTabId | undefined;
|
||||
applyBoost: {
|
||||
chatId: string;
|
||||
} & WithTabId;
|
||||
|
||||
6
src/lib/gramjs/tl/api.d.ts
vendored
6
src/lib/gramjs/tl/api.d.ts
vendored
@ -348,7 +348,7 @@ namespace Api {
|
||||
export type TypeAccessPointRule = AccessPointRule;
|
||||
export type TypeTlsClientHello = TlsClientHello;
|
||||
export type TypeTlsBlock = TlsBlockString | TlsBlockRandom | TlsBlockZero | TlsBlockDomain | TlsBlockGrease | TlsBlockScope;
|
||||
|
||||
|
||||
|
||||
export namespace storage {
|
||||
export type TypeFileType = storage.FileUnknown | storage.FilePartial | storage.FileJpeg | storage.FileGif | storage.FilePng | storage.FilePdf | storage.FileMp3 | storage.FileMov | storage.FileMp4 | storage.FileWebp;
|
||||
@ -8916,7 +8916,7 @@ namespace Api {
|
||||
}> {
|
||||
entries: Api.TypeTlsBlock[];
|
||||
};
|
||||
|
||||
|
||||
|
||||
export namespace storage {
|
||||
export class FileUnknown extends VirtualClass<void> {};
|
||||
@ -10983,7 +10983,7 @@ namespace Api {
|
||||
}>, Api.TypeDestroySessionRes> {
|
||||
sessionId: long;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export namespace auth {
|
||||
export class SendCode extends Request<Partial<{
|
||||
|
||||
@ -1473,4 +1473,4 @@ stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;
|
||||
stories.getBoostsStatus#4c449472 peer:InputPeer = stories.BoostsStatus;
|
||||
stories.getBoostersList#337ef980 peer:InputPeer offset:string limit:int = stories.BoostersList;
|
||||
stories.canApplyBoost#db05c1bd peer:InputPeer = stories.CanApplyBoostResult;
|
||||
stories.applyBoost#f29d7c2b peer:InputPeer = Bool;`;
|
||||
stories.applyBoost#f29d7c2b peer:InputPeer = Bool;`;
|
||||
@ -98,3 +98,14 @@
|
||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin side-panel-section {
|
||||
background-color: var(--color-background);
|
||||
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,6 +275,7 @@ export enum RightColumnContent {
|
||||
Search,
|
||||
Management,
|
||||
Statistics,
|
||||
BoostStatistics,
|
||||
MessageStatistics,
|
||||
StickerSearch,
|
||||
GifSearch,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user