Management / Boosts: Add giveaway and gift tabs, various fixes (#4453)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
parent
3a1c87fcb0
commit
8b31f33685
@ -2,6 +2,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { ApiPremiumSection } from '../../../global/types';
|
||||
import type {
|
||||
ApiBoost,
|
||||
ApiBoostsStatus,
|
||||
ApiCheckedGiftCode,
|
||||
ApiGiveawayInfo,
|
||||
@ -223,6 +224,24 @@ export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus):
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiBoost(boost: GramJs.Boost): ApiBoost {
|
||||
const {
|
||||
userId,
|
||||
multiplier,
|
||||
expires,
|
||||
giveaway,
|
||||
gift,
|
||||
} = boost;
|
||||
|
||||
return {
|
||||
userId: userId && buildApiPeerId(userId, 'user'),
|
||||
multiplier,
|
||||
expires,
|
||||
isFromGiveaway: giveaway,
|
||||
isGift: gift,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiMyBoost(myBoost: GramJs.MyBoost): ApiMyBoost {
|
||||
const {
|
||||
date, expires, slot, cooldownUntilDate, peer,
|
||||
|
||||
@ -6,9 +6,9 @@ import type {
|
||||
OnApiUpdate,
|
||||
} from '../../types';
|
||||
|
||||
import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import {
|
||||
buildApiBoost,
|
||||
buildApiBoostsStatus,
|
||||
buildApiCheckedGiftCode,
|
||||
buildApiGiveawayInfo,
|
||||
@ -266,15 +266,18 @@ export async function fetchBoostStatus({
|
||||
|
||||
export async function fetchBoostList({
|
||||
chat,
|
||||
isGifts,
|
||||
offset = '',
|
||||
limit,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
isGifts?: boolean;
|
||||
offset?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.premium.GetBoostsList({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
gifts: isGifts || undefined,
|
||||
offset,
|
||||
limit,
|
||||
}));
|
||||
@ -287,17 +290,12 @@ export async function fetchBoostList({
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
|
||||
const userBoosts = result.boosts.filter((boost) => boost.userId);
|
||||
const boosterIds = userBoosts.map((boost) => boost.userId!.toString());
|
||||
const boosters = buildCollectionByCallback(userBoosts, (boost) => (
|
||||
[boost.userId!.toString(), boost.expires]
|
||||
));
|
||||
const boostList = result.boosts.map(buildApiBoost);
|
||||
|
||||
return {
|
||||
count: result.count,
|
||||
boostList,
|
||||
users,
|
||||
boosters,
|
||||
boosterIds,
|
||||
nextOffset: result.nextOffset,
|
||||
};
|
||||
}
|
||||
|
||||
@ -132,6 +132,14 @@ export type ApiMyBoost = {
|
||||
cooldownUntil?: number;
|
||||
};
|
||||
|
||||
export type ApiBoost = {
|
||||
userId?: string;
|
||||
multiplier?: number;
|
||||
expires: number;
|
||||
isFromGiveaway?: boolean;
|
||||
isGift?: boolean;
|
||||
};
|
||||
|
||||
export type ApiGiveawayInfoActive = {
|
||||
type: 'active';
|
||||
isParticipating?: true;
|
||||
|
||||
@ -257,4 +257,8 @@
|
||||
&.hidden-user {
|
||||
--color-user: var(--color-deleted-account);
|
||||
}
|
||||
|
||||
&.unknown-user {
|
||||
background: var(--premium-gradient);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +53,7 @@ type OwnProps = {
|
||||
photo?: ApiPhoto;
|
||||
text?: string;
|
||||
isSavedMessages?: boolean;
|
||||
isUnknownUser?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
withVideo?: boolean;
|
||||
withStory?: boolean;
|
||||
@ -77,6 +78,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
text,
|
||||
isSavedMessages,
|
||||
isSavedDialog,
|
||||
isUnknownUser,
|
||||
withVideo,
|
||||
withStory,
|
||||
forPremiumPromo,
|
||||
@ -120,6 +122,10 @@ const Avatar: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
const specialIcon = useMemo(() => {
|
||||
if (isUnknownUser) {
|
||||
return 'user';
|
||||
}
|
||||
|
||||
if (isSavedMessages) {
|
||||
return isSavedDialog ? 'my-notes' : 'avatar-saved-messages';
|
||||
}
|
||||
@ -137,7 +143,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [isAnonymousForwards, isDeleted, isSavedDialog, isReplies, isSavedMessages]);
|
||||
}, [isUnknownUser, isSavedMessages, isDeleted, isReplies, isAnonymousForwards, isSavedDialog]);
|
||||
|
||||
const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
|
||||
const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl);
|
||||
@ -215,6 +221,7 @@ const Avatar: FC<OwnProps> = ({
|
||||
`Avatar size-${size}`,
|
||||
className,
|
||||
getPeerColorClass(peer),
|
||||
isUnknownUser && 'unknown-user',
|
||||
!peer && text && 'hidden-user',
|
||||
isSavedMessages && 'saved-messages',
|
||||
isAnonymousForwards && 'anonymous-forwards',
|
||||
|
||||
@ -25,7 +25,7 @@ import VerifiedIcon from './VerifiedIcon';
|
||||
import styles from './FullNameTitle.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
peer: ApiPeer;
|
||||
peer?: ApiPeer;
|
||||
className?: string;
|
||||
noVerified?: boolean;
|
||||
noFake?: boolean;
|
||||
@ -34,9 +34,11 @@ type OwnProps = {
|
||||
isSavedMessages?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
noLoopLimit?: boolean;
|
||||
isUnknownUser?: boolean;
|
||||
canCopyTitle?: boolean;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
observeIntersection?: ObserveFn;
|
||||
iconElement?: React.ReactNode;
|
||||
};
|
||||
|
||||
const FullNameTitle: FC<OwnProps> = ({
|
||||
@ -52,13 +54,26 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
canCopyTitle,
|
||||
onEmojiStatusClick,
|
||||
observeIntersection,
|
||||
iconElement,
|
||||
isUnknownUser,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const { showNotification } = getActions();
|
||||
const isUser = isUserId(peer.id);
|
||||
const title = isUser ? getUserFullName(peer as ApiUser) : getChatTitle(lang, peer as ApiChat);
|
||||
const isUser = peer && isUserId(peer.id);
|
||||
const isPremium = isUser && (peer as ApiUser).isPremium;
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (isUnknownUser) {
|
||||
return lang('BoostingToBeDistributed');
|
||||
}
|
||||
|
||||
if (peer && isUserId(peer.id)) {
|
||||
return getUserFullName(peer as ApiUser);
|
||||
}
|
||||
|
||||
return peer && getChatTitle(lang, peer as ApiChat);
|
||||
}, [isUnknownUser, lang, peer]);
|
||||
|
||||
const handleTitleClick = useLastCallback((e) => {
|
||||
if (!title || !canCopyTitle) {
|
||||
return;
|
||||
@ -74,16 +89,16 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
return lang(isSavedDialog ? 'MyNotes' : 'SavedMessages');
|
||||
}
|
||||
|
||||
if (isAnonymousForwardsChat(peer.id)) {
|
||||
if (peer && isAnonymousForwardsChat(peer.id)) {
|
||||
return lang('AnonymousForward');
|
||||
}
|
||||
|
||||
if (isChatWithRepliesBot(peer.id)) {
|
||||
if (peer && isChatWithRepliesBot(peer.id)) {
|
||||
return lang('RepliesTitle');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [isSavedDialog, isSavedMessages, lang, peer.id]);
|
||||
}, [isSavedDialog, isSavedMessages, lang, peer]);
|
||||
|
||||
if (specialTitle) {
|
||||
return (
|
||||
@ -103,18 +118,23 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
>
|
||||
{renderText(title || '')}
|
||||
</h3>
|
||||
{!noVerified && peer.isVerified && <VerifiedIcon />}
|
||||
{!noFake && peer.fakeType && <FakeIcon fakeType={peer.fakeType} />}
|
||||
{withEmojiStatus && peer.emojiStatus && (
|
||||
<CustomEmoji
|
||||
documentId={peer.emojiStatus.documentId}
|
||||
size={emojiStatusSize}
|
||||
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
onClick={onEmojiStatusClick}
|
||||
/>
|
||||
{!iconElement && peer && (
|
||||
<>
|
||||
{!noVerified && peer?.isVerified && <VerifiedIcon />}
|
||||
{!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />}
|
||||
{withEmojiStatus && peer.emojiStatus && (
|
||||
<CustomEmoji
|
||||
documentId={peer.emojiStatus.documentId}
|
||||
size={emojiStatusSize}
|
||||
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
onClick={onEmojiStatusClick}
|
||||
/>
|
||||
)}
|
||||
{withEmojiStatus && !peer.emojiStatus && isPremium && <PremiumIcon />}
|
||||
</>
|
||||
)}
|
||||
{withEmojiStatus && !peer.emojiStatus && isPremium && <PremiumIcon />}
|
||||
{iconElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
background: #F1F3F5;
|
||||
background: var(--color-background-menu-separator);
|
||||
border-radius: 0.625rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.withBadge {
|
||||
|
||||
@ -9,7 +9,9 @@ import type { StoryViewerOrigin } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { getMainUsername, getUserStatus, isUserOnline } from '../../global/helpers';
|
||||
import {
|
||||
getMainUsername, getUserStatus, isUserOnline,
|
||||
} from '../../global/helpers';
|
||||
import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from './helpers/renderText';
|
||||
@ -36,10 +38,13 @@ type OwnProps = {
|
||||
withMediaViewer?: boolean;
|
||||
withUsername?: boolean;
|
||||
withStory?: boolean;
|
||||
isUnknownUser?: boolean;
|
||||
withFullInfo?: boolean;
|
||||
withUpdatingStatus?: boolean;
|
||||
storyViewerOrigin?: StoryViewerOrigin;
|
||||
noEmojiStatus?: boolean;
|
||||
noFake?: boolean;
|
||||
noVerified?: boolean;
|
||||
emojiStatusSize?: number;
|
||||
noStatusOrTyping?: boolean;
|
||||
noRtl?: boolean;
|
||||
@ -47,6 +52,8 @@ type OwnProps = {
|
||||
isSavedDialog?: boolean;
|
||||
className?: string;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
iconElement?: React.ReactNode;
|
||||
rightElement?: React.ReactNode;
|
||||
};
|
||||
|
||||
type StateProps =
|
||||
@ -73,6 +80,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
emojiStatusSize,
|
||||
noStatusOrTyping,
|
||||
noEmojiStatus,
|
||||
noFake,
|
||||
noVerified,
|
||||
isUnknownUser,
|
||||
noRtl,
|
||||
user,
|
||||
userStatus,
|
||||
@ -86,6 +96,8 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
storyViewerOrigin,
|
||||
isSynced,
|
||||
onEmojiStatusClick,
|
||||
iconElement,
|
||||
rightElement,
|
||||
}) => {
|
||||
const {
|
||||
loadFullUser,
|
||||
@ -119,7 +131,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const mainUsername = useMemo(() => user && withUsername && getMainUsername(user), [user, withUsername]);
|
||||
|
||||
if (!user) {
|
||||
if (!user && !isUnknownUser) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -183,11 +195,15 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<FullNameTitle
|
||||
peer={user!}
|
||||
noFake={noFake}
|
||||
noVerified={noVerified}
|
||||
withEmojiStatus={!noEmojiStatus}
|
||||
emojiStatusSize={emojiStatusSize}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isSavedDialog={isSavedDialog}
|
||||
onEmojiStatusClick={onEmojiStatusClick}
|
||||
isUnknownUser={isUnknownUser}
|
||||
iconElement={iconElement}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -204,11 +220,12 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
<Avatar
|
||||
key={user.id}
|
||||
key={user?.id}
|
||||
size={avatarSize}
|
||||
peer={user}
|
||||
className={buildClassName(isSavedDialog && 'overlay-avatar')}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isUnknownUser={isUnknownUser}
|
||||
isSavedDialog={isSavedDialog}
|
||||
withStory={withStory}
|
||||
storyViewerOrigin={storyViewerOrigin}
|
||||
@ -220,6 +237,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
|
||||
</div>
|
||||
{ripple && <RippleEffect />}
|
||||
{rightElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,7 +18,7 @@ import mastercardIconPath from '../../assets/mastercard.svg';
|
||||
import mirIconPath from '../../assets/mir.svg';
|
||||
import visaIconPath from '../../assets/visa.svg';
|
||||
|
||||
const CARD_NUMBER_MAX_LENGTH = 23;
|
||||
const CARD_NUMBER_MAX_LENGTH = 19;
|
||||
|
||||
export type OwnProps = {
|
||||
value: string;
|
||||
|
||||
@ -529,7 +529,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
|
||||
switch (step) {
|
||||
case PaymentStep.Checkout:
|
||||
return Boolean(invoice?.isRecurring && !isTosAccepted);
|
||||
return !isTosAccepted;
|
||||
|
||||
case PaymentStep.PaymentInfo:
|
||||
return Boolean(
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
font-size: 0.9375rem;
|
||||
padding: 0.75rem 0 0 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@ -17,7 +18,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem;
|
||||
padding: 0.625rem;
|
||||
@include mixins.side-panel-section;
|
||||
}
|
||||
|
||||
@ -72,18 +73,36 @@
|
||||
}
|
||||
|
||||
.floatingBadgeButtonColor {
|
||||
padding: 0.25rem 0.75rem 0.375rem 0.5625rem;
|
||||
padding: 0 0.5rem 0 0.375rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-primary-opacity);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.floatingBadgeWarning {
|
||||
color: var(--color-orange);
|
||||
background: var( --color-light-coral);
|
||||
}
|
||||
|
||||
.floatingBadgeButton {
|
||||
padding: 0.125rem 0.75rem 0.125rem 0.5625rem;
|
||||
}
|
||||
|
||||
.floatingBadgeIcon {
|
||||
font-size: 1.125rem;
|
||||
margin-right: 0.1875rem;
|
||||
font-size: 0.875rem;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
.floatingBadgeValue {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.boostSection {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiBoostStatistics, ApiPrepaidGiveaway } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { GIVEAWAY_BOOST_PER_PREMIUM } from '../../../config';
|
||||
import {
|
||||
GIVEAWAY_BOOST_PER_PREMIUM,
|
||||
} from '../../../config';
|
||||
import { isChatChannel } from '../../../global/helpers';
|
||||
import { selectChat, selectIsGiveawayGiftsPurchaseAvailable, selectTabState } from '../../../global/selectors';
|
||||
import {
|
||||
selectChat,
|
||||
selectIsGiveawayGiftsPurchaseAvailable,
|
||||
selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateAtTime } from '../../../util/date/dateFormat';
|
||||
import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
|
||||
@ -21,6 +29,8 @@ import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import TabList from '../../ui/TabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import StatisticsOverview from './StatisticsOverview';
|
||||
|
||||
import styles from './BoostStatistics.module.scss';
|
||||
@ -51,13 +61,20 @@ const BoostStatistics = ({
|
||||
isChannel,
|
||||
}: StateProps) => {
|
||||
const {
|
||||
openChat, loadMoreBoosters, closeBoostStatistics, openGiveawayModal,
|
||||
openChat, loadMoreBoosters, closeBoostStatistics, openGiveawayModal, showNotification,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const transitionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isLoaded = boostStatistics?.boostStatus;
|
||||
const status = isLoaded ? boostStatistics.boostStatus : undefined;
|
||||
|
||||
const isGiftListEqual = boostStatistics && boostStatistics?.boosts?.count
|
||||
=== boostStatistics?.giftedBoosts?.count;
|
||||
const shouldDisplayGiftList = !isGiftListEqual && boostStatistics?.giftedBoosts
|
||||
&& boostStatistics?.giftedBoosts?.list?.length > 0;
|
||||
|
||||
const {
|
||||
currentLevel,
|
||||
hasNextLevel,
|
||||
@ -90,30 +107,140 @@ const BoostStatistics = ({
|
||||
} 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;
|
||||
const tabs = useMemo(() => {
|
||||
if (shouldDisplayGiftList) {
|
||||
return [
|
||||
{ type: 'boostList', title: lang('BoostingBoostsCount', boostStatistics?.boosts?.count) },
|
||||
{ type: 'giftedBoostList', title: lang('BoostingGiftsCount', boostStatistics?.giftedBoosts?.count) },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}, [shouldDisplayGiftList, boostStatistics?.boosts?.count, boostStatistics?.giftedBoosts?.count, lang]);
|
||||
|
||||
const initialTab = useMemo(() => {
|
||||
return boostStatistics?.boosts && boostStatistics.boosts?.list.length > 0 ? 1 : 0;
|
||||
}, [boostStatistics]);
|
||||
|
||||
const handleBoosterClick = useLastCallback((userId: string) => {
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
|
||||
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
|
||||
|
||||
const tabType = tabs[renderingActiveTab]?.type;
|
||||
|
||||
const activeKey = tabs.findIndex(({ type }) => type === tabType);
|
||||
|
||||
const boostersToLoadCount = useMemo(() => {
|
||||
if (!boostStatistics) return undefined;
|
||||
|
||||
const list = shouldDisplayGiftList ? (tabType === 'boostList'
|
||||
? boostStatistics.boosts : boostStatistics.giftedBoosts) : boostStatistics.boosts;
|
||||
if (!list?.count) return undefined;
|
||||
|
||||
const loadedBoostsCount = list.list.reduce((total, boost) => {
|
||||
return total + (boost.multiplier || 1);
|
||||
}, 0);
|
||||
|
||||
const totalCount = list.count;
|
||||
const toLoadCount = totalCount - loadedBoostsCount;
|
||||
|
||||
return toLoadCount > 0 ? toLoadCount : undefined;
|
||||
}, [shouldDisplayGiftList, boostStatistics, tabType]);
|
||||
|
||||
const renderBoostIcon = useLastCallback((multiplier: number) => (
|
||||
<div className={styles.quantity}>
|
||||
<div className={buildClassName(styles.floatingBadge, styles.floatingBadgeButtonColor)}>
|
||||
<Icon name="boost" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue}>{multiplier}</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
const renderBoostTypeIcon = useLastCallback((boost) => {
|
||||
if (!boost.isFromGiveaway && !boost.isGift) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.quantity}>
|
||||
<div className={buildClassName(styles.floatingBadge,
|
||||
!boost.giveaway && styles.floatingBadgeWarning,
|
||||
styles.floatingBadgeButtonColor,
|
||||
styles.floatingBadgeButton)}
|
||||
>
|
||||
<Icon name="gift" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue}>{lang(boost.giveaway
|
||||
? 'lng_prizes_results_link' : 'BoostingGift')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleBoosterClick = useLastCallback((userId?: string) => {
|
||||
if (!userId) {
|
||||
showNotification({
|
||||
message: lang('BoostingRecipientWillBeSelected'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
openChat({ id: userId });
|
||||
closeBoostStatistics();
|
||||
});
|
||||
|
||||
const renderBoostList = useLastCallback((boost) => {
|
||||
return (
|
||||
<ListItem
|
||||
className="chat-item-clickable"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleBoosterClick(boost.userId)}
|
||||
>
|
||||
<PrivateChatInfo
|
||||
className={styles.user}
|
||||
userId={boost.userId}
|
||||
status={lang('BoostExpireOn', formatDateAtTime(lang, boost.expires * 1000))}
|
||||
noEmojiStatus
|
||||
forceShowSelf
|
||||
noFake
|
||||
noVerified
|
||||
isUnknownUser={!boost.userId}
|
||||
iconElement={boost.multiplier ? renderBoostIcon(boost.multiplier) : undefined}
|
||||
rightElement={renderBoostTypeIcon(boost)}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
|
||||
const handleGiveawayClick = useLastCallback(() => {
|
||||
openGiveawayModal({ chatId });
|
||||
});
|
||||
|
||||
const handleLoadMore = useLastCallback(() => {
|
||||
loadMoreBoosters();
|
||||
loadMoreBoosters({ isGifts: tabType === 'giftedBoostList' });
|
||||
});
|
||||
|
||||
const launchPrepaidGiveawayHandler = useLastCallback((prepaidGiveaway: ApiPrepaidGiveaway) => {
|
||||
openGiveawayModal({ chatId, prepaidGiveaway });
|
||||
});
|
||||
|
||||
function renderContent() {
|
||||
let listToRender;
|
||||
if (tabType === 'boostList') {
|
||||
listToRender = boostStatistics?.boosts?.list;
|
||||
} else if (tabType === 'giftedBoostList') {
|
||||
listToRender = boostStatistics?.giftedBoosts?.list;
|
||||
}
|
||||
|
||||
if (listToRender && !listToRender?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{listToRender?.map((boost) => renderBoostList(boost))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'custom-scroll')}>
|
||||
{!isLoaded && <Loading />}
|
||||
@ -152,7 +279,10 @@ const BoostStatistics = ({
|
||||
<p className={styles.month}>{lang('PrepaidGiveawayMonths', prepaidGiveaway.months)}</p>
|
||||
</div>
|
||||
<div className={styles.quantity}>
|
||||
<div className={buildClassName(styles.floatingBadge, styles.floatingBadgeButtonColor)}>
|
||||
<div className={buildClassName(styles.floatingBadge,
|
||||
styles.floatingBadgeButtonColor,
|
||||
styles.floatingBadgeButton)}
|
||||
>
|
||||
<Icon name="boost" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{prepaidGiveaway.quantity * (giveawayBoostsPerPremium ?? GIVEAWAY_BOOST_PER_PREMIUM)}
|
||||
@ -165,46 +295,52 @@ const BoostStatistics = ({
|
||||
<p className="text-muted hint" key="links-hint">{lang('BoostingSelectPaidGiveaway')}</p>
|
||||
</div>
|
||||
)}
|
||||
{isChannel && (
|
||||
<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)}
|
||||
<div className={styles.section}>
|
||||
{shouldDisplayGiftList ? (
|
||||
<div
|
||||
className={styles.boostSection}
|
||||
>
|
||||
<Transition
|
||||
key={activeKey}
|
||||
ref={transitionRef}
|
||||
name={lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'}
|
||||
activeKey={activeKey}
|
||||
renderCount={tabs.length}
|
||||
shouldRestoreHeight
|
||||
className="shared-media-transition"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
{renderContent()}
|
||||
</Transition>
|
||||
<TabList big activeTab={renderingActiveTab} tabs={tabs} onSwitchTab={setActiveTab} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h4 className={styles.sectionHeader} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('BoostingBoostsCount', boostStatistics?.boosts?.count)}
|
||||
</h4>
|
||||
{!boostStatistics?.boosts?.list?.length && (
|
||||
<div className={styles.noResults}>{lang(isChannel ? 'NoBoostersHint' : 'NoBoostersGroupHint')}
|
||||
</div>
|
||||
)}
|
||||
{boostStatistics?.boosts?.list?.map((boost) => renderBoostList(boost))}
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
<LinkField className={styles.section} link={status!.boostUrl} withShare title={lang('LinkForBoosting')} />
|
||||
{isGiveawayAvailable && (
|
||||
<div className={styles.section}>
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
&.big {
|
||||
font-size: 1rem;
|
||||
--border-radius-messages-small: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@ -5,7 +5,7 @@ import { PaymentStep } from '../../../types';
|
||||
|
||||
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey, unique } from '../../../util/iteratees';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import * as langProvider from '../../../util/langProvider';
|
||||
import { getStripeError } from '../../../util/payments/stripe';
|
||||
import { buildQueryString } from '../../../util/requestQuery';
|
||||
@ -578,13 +578,15 @@ addActionHandler('openBoostStatistics', async (global, actions, payload): Promis
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const [boostsListResult, boostStatusResult] = await Promise.all([
|
||||
const [boostListResult, boostListGiftResult,
|
||||
boostStatusResult] = await Promise.all([
|
||||
callApi('fetchBoostList', { chat }),
|
||||
callApi('fetchBoostList', { chat, isGifts: true }),
|
||||
callApi('fetchBoostStatus', { chat }),
|
||||
]);
|
||||
|
||||
global = getGlobal();
|
||||
if (!boostsListResult || !boostStatusResult) {
|
||||
if (!boostListResult || !boostListGiftResult || !boostStatusResult) {
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: undefined,
|
||||
}, tabId);
|
||||
@ -592,22 +594,28 @@ addActionHandler('openBoostStatistics', async (global, actions, payload): Promis
|
||||
return;
|
||||
}
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(boostsListResult.users, 'id'));
|
||||
const totalBoostUserList = [...boostListResult.users, ...boostListGiftResult.users];
|
||||
global = addUsers(global, buildCollectionByKey(totalBoostUserList, 'id'));
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
boostStatus: boostStatusResult,
|
||||
boosters: boostsListResult.boosters,
|
||||
boosterIds: boostsListResult.boosterIds,
|
||||
count: boostsListResult.count,
|
||||
nextOffset: boostsListResult.nextOffset,
|
||||
nextOffset: boostListResult.nextOffset,
|
||||
boosts: {
|
||||
count: boostListResult.count,
|
||||
list: boostListResult.boostList,
|
||||
},
|
||||
giftedBoosts: {
|
||||
count: boostListGiftResult?.count,
|
||||
list: boostListGiftResult?.boostList,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { isGifts, tabId = getCurrentTabId() } = payload || {};
|
||||
let tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
@ -625,6 +633,7 @@ addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<v
|
||||
const result = await callApi('fetchBoostList', {
|
||||
chat,
|
||||
offset: tabState.boostStatistics.nextOffset,
|
||||
isGifts,
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
@ -634,17 +643,19 @@ addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<v
|
||||
tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
const updatedBoostList = (isGifts
|
||||
? tabState.boostStatistics.giftedBoosts?.list || []
|
||||
: tabState.boostStatistics.boosts?.list || []).concat(result.boostList);
|
||||
|
||||
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,
|
||||
[isGifts ? 'giftedBoosts' : 'boosts']: {
|
||||
count: result.count,
|
||||
list: updatedBoostList,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
@ -3,6 +3,7 @@ import type {
|
||||
ApiAttachBot,
|
||||
ApiAttachment,
|
||||
ApiAvailableReaction,
|
||||
ApiBoost,
|
||||
ApiBoostsStatus,
|
||||
ApiChannelStatistics,
|
||||
ApiChat,
|
||||
@ -694,12 +695,17 @@ export type TabState = {
|
||||
|
||||
boostStatistics?: {
|
||||
chatId: string;
|
||||
boosters?: Record<string, number>;
|
||||
boosterIds?: string[];
|
||||
boostStatus?: ApiBoostsStatus;
|
||||
isLoadingBoosters?: boolean;
|
||||
nextOffset?: string;
|
||||
count?: number;
|
||||
boosts?: {
|
||||
count: number;
|
||||
list: ApiBoost[];
|
||||
};
|
||||
giftedBoosts?: {
|
||||
count: number;
|
||||
list: ApiBoost[];
|
||||
};
|
||||
};
|
||||
|
||||
giftCodeModal?: {
|
||||
@ -2356,7 +2362,7 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
} & WithTabId;
|
||||
closeBoostStatistics: WithTabId | undefined;
|
||||
loadMoreBoosters: WithTabId | undefined;
|
||||
loadMoreBoosters: { isGifts?: boolean } & WithTabId | undefined;
|
||||
applyBoost: {
|
||||
slots: number[];
|
||||
chatId: string;
|
||||
|
||||
@ -30,6 +30,8 @@ $color-error: #e53935;
|
||||
$color-warning: #fb8c00;
|
||||
|
||||
$color-yellow: #fdd764;
|
||||
$color-orange: #d08a31;
|
||||
$color-light-coral: #d08a3133;
|
||||
|
||||
$color-white: #ffffff;
|
||||
$color-black: #000000;
|
||||
@ -115,6 +117,9 @@ $color-message-story-mention-to: #74bcff;
|
||||
|
||||
--color-yellow: #{$color-yellow};
|
||||
|
||||
--color-orange: #{$color-orange};
|
||||
--color-light-coral: #{$color-light-coral};
|
||||
|
||||
--color-links: #{$color-links};
|
||||
|
||||
--color-own-links: #{$color-white};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user