Gifts: Show more info (#5114)

This commit is contained in:
zubiden 2024-11-02 21:11:39 +04:00 committed by Alexander Zinchuk
parent ebe9377b41
commit 699dfa4a1e
11 changed files with 166 additions and 60 deletions

View File

@ -85,6 +85,7 @@ export interface GramJsAppConfig extends LimitsConfig {
upload_premium_speedup_upload?: number;
stars_gifts_enabled?: boolean;
stargifts_message_length_max?: number;
stargifts_convert_period_max?: number;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -169,5 +170,6 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled,
isStarsGiftEnabled: appConfig.stars_gifts_enabled,
starGiftMaxMessageLength: appConfig.stargifts_message_length_max,
starGiftMaxConvertPeriod: appConfig.stargifts_convert_period_max,
};
}

View File

@ -585,7 +585,8 @@ export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): Ap
export function buildApiStarGift(startGift: GramJs.StarGift): ApiStarGift {
const {
id, limited, sticker, stars, availabilityRemains, availabilityTotal, convertStars,
id, limited, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate,
soldOut,
} = startGift;
return {
@ -596,6 +597,9 @@ export function buildApiStarGift(startGift: GramJs.StarGift): ApiStarGift {
availabilityRemains,
availabilityTotal,
starsToConvert: convertStars.toJSNumber(),
firstSaleDate,
lastSaleDate,
isSoldOut: soldOut,
};
}

View File

@ -237,6 +237,7 @@ export interface ApiAppConfig {
isChannelRevenueWithdrawalEnabled?: boolean;
isStarsGiftEnabled?: boolean;
starGiftMaxMessageLength?: number;
starGiftMaxConvertPeriod?: number;
}
export interface ApiConfig {

View File

@ -196,6 +196,9 @@ export type ApiStarGift = {
availabilityRemains?: number;
availabilityTotal?: number;
starsToConvert: number;
isSoldOut?: true;
firstSaleDate?: number;
lastSaleDate?: number;
};
export interface ApiUserStarGift {

View File

@ -1296,7 +1296,6 @@
"GiftSoldCount" = "{count} sold";
"GiftLeftCount" = "{count} left";
"GiftSoldOut" = "sold out";
"GiftSoldOutInfo" = "Sorry, this gift is sold out.";
"GiftMessagePlaceholder" = "Enter Message (Optional)";
"GiftHideMyName" = "Hide My Name";
"GiftHideNameDescription" = "Hide my name and message from visitors to {profile}'s profile. {receiver} will still see your name and message.";
@ -1320,10 +1319,21 @@
"GiftInfoConvert_one" = "Convert to {amount} Star";
"GiftInfoConvert_other" = "Convert to {amount} Stars";
"GiftInfoConvertTitle" = "Convert Gift to Stars";
"GiftInfoConvertDescription" = "Do you want to convert this gift from **{user}** to **{amount}**?\n\nThis action cannot be undone. This will permanently destroy the gift.";
"GiftInfoConvertDescription1" = "Do you want to convert this gift from **{user}** to **{amount}**?";
"GiftInfoConvertDescription2" = "This action cannot be undone. This will permanently destroy the gift.";
"GiftInfoConvertDescriptionPeriod_one" = "Conversion is available for the next **{count} days**."
"GiftInfoConvertDescriptionPeriod_other" = "Conversion is available for the next **{count} days**."
"GiftInfoSaved" = "This gift is visible on your profile. {link}";
"GiftInfoSavedView" = "View >";
"GiftInfoHidden" = "This gift is hidden. Only you can see it.";
"GiftInfoAvailability" = "Availability";
"GiftInfoAvailabilityValue_one" = "{count} of {total} left";
"GiftInfoAvailabilityValue_other" = "{count} of {total} left";
"GiftInfoFirstSale" = "First Sale";
"GiftInfoLastSale" = "Last Sale";
"GiftInfoSoldOutTitle" = "Unavailable";
"GiftInfoSoldOutDescription" = "This gift has been sold out";
"GiftInfoSenderHidden" = "Only you can see the sender's name and message.";
"StarsAmount" = "⭐️{amount}";
"StarsAmountText_one" = "{amount} Star";
"StarsAmountText_other" = "{amount} Stars";

View File

@ -30,21 +30,18 @@ export type StateProps = {
const GIFT_STICKER_SIZE = 90;
function GiftItemStar({ sticker, gift, onClick }: OwnProps & StateProps) {
const { showNotification } = getActions();
const { openGiftInfoModal } = getActions();
const lang = useLang();
const {
stars,
isLimited,
availabilityRemains,
availabilityTotal,
isSoldOut,
} = gift;
const isSoldOut = availabilityTotal && !availabilityRemains;
const handleGiftClick = useLastCallback(() => {
if (isSoldOut) {
showNotification({ message: lang('GiftSoldOutInfo') });
openGiftInfoModal({ gift });
return;
}

View File

@ -19,6 +19,10 @@
margin-bottom: 0;
}
.soldOut {
color: var(--color-error);
}
.description {
text-align: center;
}

View File

@ -7,6 +7,7 @@ import type { TabState } from '../../../../global/types';
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { getUserFullName } from '../../../../global/helpers';
import { selectStarGiftSticker, selectUser } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
import { formatInteger } from '../../../../util/textFormat';
@ -38,12 +39,13 @@ type StateProps = {
userFrom?: ApiUser;
targetUser?: ApiUser;
currentUserId?: string;
starGiftMaxConvertPeriod?: number;
};
const STICKER_SIZE = 120;
const GiftInfoModal = ({
modal, sticker, userFrom, targetUser, currentUserId,
modal, sticker, userFrom, targetUser, currentUserId, starGiftMaxConvertPeriod,
}: OwnProps & StateProps) => {
const {
closeGiftInfoModal,
@ -59,9 +61,14 @@ const GiftInfoModal = ({
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const { gift: userGift } = renderingModal || {};
const { gift: typeGift } = renderingModal || {};
const isUserGift = typeGift && 'gift' in typeGift;
const userGift = isUserGift ? typeGift : undefined;
const canUpdate = Boolean(userGift?.fromId && userGift.messageId);
const isSender = userGift?.fromId === currentUserId;
const canConvertDifference = (userGift && starGiftMaxConvertPeriod && (
userGift.date + starGiftMaxConvertPeriod - Date.now() / 1000
)) || 0;
const handleClose = useLastCallback(() => {
closeGiftInfoModal();
@ -86,16 +93,23 @@ const GiftInfoModal = ({
});
const modalData = useMemo(() => {
if (!userGift) {
if (!typeGift) {
return undefined;
}
const {
gift, date, fromId, isNameHidden, message, starsToConvert, isUnsaved, isConverted,
} = userGift;
fromId, isNameHidden, message, starsToConvert, isUnsaved, isConverted,
} = userGift || {};
const gift = isUserGift ? typeGift.gift : typeGift;
const isVisibleForMe = isNameHidden && targetUser;
const description = (() => {
if (!userGift) {
return lang('GiftInfoSoldOutDescription');
}
if (!canUpdate && !isSender) return undefined;
if (!starsToConvert || canConvertDifference < 0) return undefined;
if (isConverted) {
return canUpdate
? lang('GiftInfoDescriptionConverted', {
@ -135,16 +149,19 @@ const GiftInfoModal = ({
<div className={styles.header}>
<AnimatedIconFromSticker sticker={sticker} noLoop nonInteractive size={STICKER_SIZE} />
<h1 className={styles.title}>
{lang(canUpdate ? 'GiftInfoReceived' : 'GiftInfoTitle')}
{!userGift && lang('GiftInfoSoldOutTitle')}
{userGift && lang(canUpdate ? 'GiftInfoReceived' : 'GiftInfoTitle')}
</h1>
<p className={styles.amount}>
<span className={styles.amount}>
{formatInteger(gift.stars)}
</span>
<StarIcon type="gold" size="middle" />
</p>
{userGift && (
<p className={styles.amount}>
<span className={styles.amount}>
{formatInteger(gift.stars)}
</span>
<StarIcon type="gold" size="middle" />
</p>
)}
{description && (
<p className={styles.description}>
<p className={buildClassName(styles.description, !userGift && styles.soldOut)}>
{description}
</p>
)}
@ -164,10 +181,26 @@ const GiftInfoModal = ({
]);
}
tableData.push([
lang('GiftInfoDate'),
formatDateTimeToString(date * 1000, lang.code, true),
]);
if (userGift?.date) {
tableData.push([
lang('GiftInfoDate'),
formatDateTimeToString(userGift.date * 1000, lang.code, true),
]);
}
if (gift.firstSaleDate) {
tableData.push([
lang('GiftInfoFirstSale'),
formatDateTimeToString(gift.firstSaleDate * 1000, lang.code, true),
]);
}
if (gift.lastSaleDate) {
tableData.push([
lang('GiftInfoLastSale'),
formatDateTimeToString(gift.lastSaleDate * 1000, lang.code, true),
]);
}
tableData.push([
lang('GiftInfoValue'),
@ -180,7 +213,7 @@ const GiftInfoModal = ({
[STARS_ICON_PLACEHOLDER]: <StarIcon type="gold" size="small" />,
},
})}
{canUpdate && Boolean(starsToConvert) && (
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
<BadgeButton onClick={openConvertConfirm}>
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
</BadgeButton>
@ -188,6 +221,16 @@ const GiftInfoModal = ({
</div>,
]);
if (gift.availabilityTotal) {
tableData.push([
lang('GiftInfoAvailability'),
lang('GiftInfoAvailabilityValue', {
count: formatInteger(gift.availabilityRemains!),
total: formatInteger(gift.availabilityTotal),
}),
]);
}
if (message) {
tableData.push([
undefined,
@ -198,14 +241,21 @@ const GiftInfoModal = ({
const footer = (
<div className={styles.footer}>
{canUpdate && (
<p className={styles.footerDescription}>
{isUnsaved ? lang('GiftInfoHidden')
: lang('GiftInfoSaved', {
link: <Link isPrimary onClick={handleOpenProfile}>{lang('GiftInfoSavedView')}</Link>,
}, {
withNodes: true,
})}
</p>
<div className={styles.footerDescription}>
<div>
{isUnsaved ? lang('GiftInfoHidden')
: lang('GiftInfoSaved', {
link: <Link isPrimary onClick={handleOpenProfile}>{lang('GiftInfoSavedView')}</Link>,
}, {
withNodes: true,
})}
</div>
{isVisibleForMe && (
<div>
{lang('GiftInfoSenderHidden')}
</div>
)}
</div>
)}
{!canUpdate && (
<Button size="smaller" onClick={handleClose}>
@ -225,7 +275,7 @@ const GiftInfoModal = ({
tableData,
footer,
};
}, [userGift, sticker, lang, canUpdate, isSender, oldLang, targetUser]);
}, [typeGift, userGift, isUserGift, targetUser, sticker, lang, canUpdate, canConvertDifference, isSender, oldLang]);
return (
<>
@ -236,31 +286,48 @@ const GiftInfoModal = ({
footer={modalData?.footer}
onClose={handleClose}
/>
<ConfirmDialog
isOpen={isConvertConfirmOpen}
onClose={closeConvertConfirm}
confirmHandler={handleConvertToStars}
title={lang('GiftInfoConvertTitle')}
>
{userGift && lang('GiftInfoConvertDescription', {
amount: lang('StarsAmountText', { amount: formatInteger(userGift.starsToConvert!) }),
user: getUserFullName(userFrom)!,
}, {
withNodes: true,
withMarkdown: true,
renderTextFilters: ['br'],
})}
</ConfirmDialog>
{userGift && (
<ConfirmDialog
isOpen={isConvertConfirmOpen}
onClose={closeConvertConfirm}
confirmHandler={handleConvertToStars}
title={lang('GiftInfoConvertTitle')}
>
<div>
{lang('GiftInfoConvertDescription1', {
amount: lang('StarsAmountText', { amount: formatInteger(userGift.starsToConvert!) }),
user: getUserFullName(userFrom)!,
}, {
withNodes: true,
withMarkdown: true,
})}
</div>
{canConvertDifference > 0 && (
<div>
{lang('GiftInfoConvertDescriptionPeriod', {
count: formatInteger(Math.ceil(canConvertDifference / 60 / 60 / 24)),
}, {
withNodes: true,
withMarkdown: true,
})}
</div>
)}
<div>{lang('GiftInfoConvertDescription2')}</div>
</ConfirmDialog>
)}
</>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const stickerId = modal?.gift?.gift.stickerId;
const typeGift = modal?.gift;
const isUserGift = typeGift && 'gift' in typeGift;
const gift = isUserGift ? typeGift.gift : typeGift;
const stickerId = gift?.stickerId;
const sticker = stickerId ? selectStarGiftSticker(global, stickerId) : undefined;
const fromId = modal?.gift?.fromId;
const fromId = isUserGift && typeGift.fromId;
const userFrom = fromId ? selectUser(global, fromId) : undefined;
const targetUser = modal?.userId ? selectUser(global, modal.userId) : undefined;
@ -269,6 +336,7 @@ export default memo(withGlobal<OwnProps>(
userFrom,
targetUser,
currentUserId: global.currentUserId,
starGiftMaxConvertPeriod: global.appConfig?.starGiftMaxConvertPeriod,
};
},
)(GiftInfoModal));

View File

@ -264,9 +264,11 @@ addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): Act
addActionHandler('openGiftInfoModal', (global, actions, payload): ActionReturnType => {
const {
userId, gift, tabId = getCurrentTabId(),
gift, tabId = getCurrentTabId(),
} = payload;
const userId = 'userId' in payload ? payload.userId : undefined;
return updateTabState(global, {
giftInfoModal: {
userId,

View File

@ -882,8 +882,8 @@ export type TabState = {
};
giftInfoModal?: {
userId: string;
gift: ApiUserStarGift;
userId?: string;
gift: ApiUserStarGift | ApiStarGift;
};
};
@ -3461,10 +3461,12 @@ export interface ActionPayloads {
chatId: string;
messageId: number;
} & WithTabId;
openGiftInfoModal: {
openGiftInfoModal: ({
userId: string;
gift: ApiUserStarGift;
} & WithTabId;
} | {
gift: ApiStarGift;
}) & WithTabId;
closeGiftInfoModal: WithTabId | undefined;
loadUserGifts: {
userId: string;

View File

@ -1563,7 +1563,6 @@ export interface LangPair {
'count': string | number;
};
'GiftSoldOut': undefined;
'GiftSoldOutInfo': undefined;
'GiftMessagePlaceholder': undefined;
'GiftHideMyName': undefined;
'GiftHideNameDescription': {
@ -1599,15 +1598,29 @@ export interface LangPair {
'amount': string | number;
};
'GiftInfoConvertTitle': undefined;
'GiftInfoConvertDescription': {
'GiftInfoConvertDescription1': {
'user': string | number;
'amount': string | number;
};
'GiftInfoConvertDescription2': undefined;
'GiftInfoConvertDescriptionPeriod': {
'count': string | number;
};
'GiftInfoSaved': {
'link': string | number;
};
'GiftInfoSavedView': undefined;
'GiftInfoHidden': undefined;
'GiftInfoAvailability': undefined;
'GiftInfoAvailabilityValue': {
'count': string | number;
'total': string | number;
};
'GiftInfoFirstSale': undefined;
'GiftInfoLastSale': undefined;
'GiftInfoSoldOutTitle': undefined;
'GiftInfoSoldOutDescription': undefined;
'GiftInfoSenderHidden': undefined;
'StarsAmount': {
'amount': string | number;
};