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:
Alexander Zinchuk 2024-05-03 14:38:04 +02:00
parent 3a1c87fcb0
commit 8b31f33685
16 changed files with 357 additions and 108 deletions

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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;

View File

@ -257,4 +257,8 @@
&.hidden-user {
--color-user: var(--color-deleted-account);
}
&.unknown-user {
background: var(--premium-gradient);
}
}

View File

@ -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',

View File

@ -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>
);
};

View File

@ -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 {

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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(

View File

@ -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;
}

View File

@ -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}>

View File

@ -17,7 +17,6 @@
&.big {
font-size: 1rem;
--border-radius-messages-small: 0;
}
&::-webkit-scrollbar {

View File

@ -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);

View File

@ -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;

View File

@ -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};