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; upload_premium_speedup_upload?: number;
stars_gifts_enabled?: boolean; stars_gifts_enabled?: boolean;
stargifts_message_length_max?: number; stargifts_message_length_max?: number;
stargifts_convert_period_max?: number;
} }
function buildEmojiSounds(appConfig: GramJsAppConfig) { function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -169,5 +170,6 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled, isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled,
isStarsGiftEnabled: appConfig.stars_gifts_enabled, isStarsGiftEnabled: appConfig.stars_gifts_enabled,
starGiftMaxMessageLength: appConfig.stargifts_message_length_max, 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 { export function buildApiStarGift(startGift: GramJs.StarGift): ApiStarGift {
const { const {
id, limited, sticker, stars, availabilityRemains, availabilityTotal, convertStars, id, limited, sticker, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate,
soldOut,
} = startGift; } = startGift;
return { return {
@ -596,6 +597,9 @@ export function buildApiStarGift(startGift: GramJs.StarGift): ApiStarGift {
availabilityRemains, availabilityRemains,
availabilityTotal, availabilityTotal,
starsToConvert: convertStars.toJSNumber(), starsToConvert: convertStars.toJSNumber(),
firstSaleDate,
lastSaleDate,
isSoldOut: soldOut,
}; };
} }

View File

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

View File

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

View File

@ -1296,7 +1296,6 @@
"GiftSoldCount" = "{count} sold"; "GiftSoldCount" = "{count} sold";
"GiftLeftCount" = "{count} left"; "GiftLeftCount" = "{count} left";
"GiftSoldOut" = "sold out"; "GiftSoldOut" = "sold out";
"GiftSoldOutInfo" = "Sorry, this gift is sold out.";
"GiftMessagePlaceholder" = "Enter Message (Optional)"; "GiftMessagePlaceholder" = "Enter Message (Optional)";
"GiftHideMyName" = "Hide My Name"; "GiftHideMyName" = "Hide My Name";
"GiftHideNameDescription" = "Hide my name and message from visitors to {profile}'s profile. {receiver} will still see your name and message."; "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_one" = "Convert to {amount} Star";
"GiftInfoConvert_other" = "Convert to {amount} Stars"; "GiftInfoConvert_other" = "Convert to {amount} Stars";
"GiftInfoConvertTitle" = "Convert Gift to 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}"; "GiftInfoSaved" = "This gift is visible on your profile. {link}";
"GiftInfoSavedView" = "View >"; "GiftInfoSavedView" = "View >";
"GiftInfoHidden" = "This gift is hidden. Only you can see it."; "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}"; "StarsAmount" = "⭐️{amount}";
"StarsAmountText_one" = "{amount} Star"; "StarsAmountText_one" = "{amount} Star";
"StarsAmountText_other" = "{amount} Stars"; "StarsAmountText_other" = "{amount} Stars";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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