= ({
isOpen={isOpen}
listItemData={modalData.listItemData}
headerIconName="cash-circle"
+ withSeparator
header={modalData.header}
footer={modalData.footer}
buttonText={oldLang('RevenueSharingAdsUnderstood')}
diff --git a/src/components/common/AnimatedIconFromSticker.tsx b/src/components/common/AnimatedIconFromSticker.tsx
index 40d761421..406d6b22e 100644
--- a/src/components/common/AnimatedIconFromSticker.tsx
+++ b/src/components/common/AnimatedIconFromSticker.tsx
@@ -20,7 +20,7 @@ function AnimatedIconFromSticker(props: OwnProps) {
} = props;
const thumbDataUri = sticker?.thumbnail?.dataUri;
- const localMediaHash = sticker && `sticker${sticker.id}`;
+ const localMediaHash = sticker && getStickerMediaHash(sticker, 'full');
const previewBlobUrl = useMedia(
sticker ? getStickerMediaHash(sticker, 'preview') : undefined,
noLoad && !forcePreview,
diff --git a/src/components/common/AnimatedIconWithPreview.module.scss b/src/components/common/AnimatedIconWithPreview.module.scss
index b124c6825..4faaa222f 100644
--- a/src/components/common/AnimatedIconWithPreview.module.scss
+++ b/src/components/common/AnimatedIconWithPreview.module.scss
@@ -16,6 +16,6 @@
height: 100%;
&:global(.closing) {
- transition-delay: 150ms;
+ transition-delay: 50ms;
}
}
diff --git a/src/components/common/helpers/gifts.ts b/src/components/common/helpers/gifts.ts
index c784e79be..e08318a21 100644
--- a/src/components/common/helpers/gifts.ts
+++ b/src/components/common/helpers/gifts.ts
@@ -1,6 +1,7 @@
import type {
ApiFormattedText,
ApiStarGift,
+ ApiStarGiftAttribute,
ApiStarGiftAttributeBackdrop,
ApiStarGiftAttributeModel,
ApiStarGiftAttributeOriginalDetails,
@@ -40,10 +41,14 @@ export function getGiftMessage(gift: ApiStarGift): ApiFormattedText | undefined
export function getGiftAttributes(gift: ApiStarGift): GiftAttributes | undefined {
if (gift.type !== 'starGiftUnique') return undefined;
- const model = gift.attributes.find((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model');
- const backdrop = gift.attributes.find((attr): attr is ApiStarGiftAttributeBackdrop => attr.type === 'backdrop');
- const pattern = gift.attributes.find((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern');
- const originalDetails = gift.attributes.find((attr): attr is ApiStarGiftAttributeOriginalDetails => (
+ return getGiftAttributesFromList(gift.attributes);
+}
+
+export function getGiftAttributesFromList(attributes: ApiStarGiftAttribute[]) {
+ const model = attributes.find((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model');
+ const backdrop = attributes.find((attr): attr is ApiStarGiftAttributeBackdrop => attr.type === 'backdrop');
+ const pattern = attributes.find((attr): attr is ApiStarGiftAttributePattern => attr.type === 'pattern');
+ const originalDetails = attributes.find((attr): attr is ApiStarGiftAttributeOriginalDetails => (
attr.type === 'originalDetails'
));
diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx
index a48aa6b46..d9f4bc723 100644
--- a/src/components/common/helpers/renderActionMessageText.tsx
+++ b/src/components/common/helpers/renderActionMessageText.tsx
@@ -95,7 +95,7 @@ export function renderActionMessageText(
}
if (translationKey.startsWith('Notification.StarsGift.Upgrade')) {
unprocessed = unprocessed
- .replace('%@', '%action_origin%');
+ .replace('%@', '%action_origin_chat%');
}
if (translationKey.startsWith('ActionUniqueGiftTransfer')) {
unprocessed = unprocessed
@@ -138,6 +138,18 @@ export function renderActionMessageText(
unprocessed = processed.pop() as string;
content.push(...processed);
+ processed = processPlaceholder(
+ unprocessed,
+ '%action_origin_chat%',
+ actionOriginChat ? (
+ renderChatContent(oldLang, actionOriginChat, noLinks) || NBSP
+ ) : 'Chat',
+ '',
+ );
+
+ unprocessed = processed.pop() as string;
+ content.push(...processed);
+
if (unprocessed.includes('%payment_amount%')) {
processed = processPlaceholder(
unprocessed,
diff --git a/src/components/common/profile/RadialPatternBackground.tsx b/src/components/common/profile/RadialPatternBackground.tsx
index 25d26337f..1ac3407e3 100644
--- a/src/components/common/profile/RadialPatternBackground.tsx
+++ b/src/components/common/profile/RadialPatternBackground.tsx
@@ -1,5 +1,5 @@
import React, {
- memo, useEffect, useRef, useSignal, useState,
+ memo, useEffect, useMemo, useRef, useSignal, useState,
} from '../../../lib/teact/teact';
import type { ApiSticker } from '../../../api/types';
@@ -22,6 +22,7 @@ type OwnProps = {
patternColor?: string;
patternIcon?: ApiSticker;
className?: string;
+ clearBottomSector?: boolean;
};
const RINGS = 3;
@@ -33,38 +34,11 @@ const BASE_ICON_SIZE = 20;
const MIN_SIZE = 250;
-const PATTERN_POSITIONS = (() => {
- const coordinates: { x: number; y: number; sizeFactor: number }[] = [];
- for (let ring = 1; ring <= RINGS; ring++) {
- const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT));
- const ringProgress = ring / RINGS;
- const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress;
-
- const angleShift = ring % 2 === 0 ? Math.PI / ringItemCount : 0;
-
- for (let i = 0; i < ringItemCount; i++) {
- const angle = (i / ringItemCount) * Math.PI * 2 + angleShift;
- // Slightly oval
- const xOffset = ringRadius * 1.71 * Math.cos(angle);
- const yOffset = ringRadius * Math.sin(angle);
-
- const x = 0.5 + xOffset;
- const y = 0.5 + yOffset;
-
- const sizeFactor = 1.4 - ringProgress * Math.random();
-
- coordinates.push({
- x, y, sizeFactor,
- });
- }
- }
- return coordinates;
-})();
-
const RadialPatternBackground = ({
backgroundColors,
patternColor,
patternIcon,
+ clearBottomSector,
className,
}: OwnProps) => {
// eslint-disable-next-line no-null/no-null
@@ -86,6 +60,39 @@ const RadialPatternBackground = ({
preloadImage(previewUrl).then(setEmojiImage);
}, [previewUrl]);
+ const patternPositions = useMemo(() => {
+ const coordinates: { x: number; y: number; sizeFactor: number }[] = [];
+ for (let ring = 1; ring <= RINGS; ring++) {
+ const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT));
+ const ringProgress = ring / RINGS;
+ const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress;
+
+ const angleShift = ring % 2 === 0 ? Math.PI / ringItemCount : 0;
+
+ for (let i = 0; i < ringItemCount; i++) {
+ const angle = (i / ringItemCount) * Math.PI * 2 + angleShift;
+
+ if (clearBottomSector && angle > Math.PI * 0.45 && angle < Math.PI * 0.55) {
+ continue;
+ }
+
+ // Slightly oval
+ const xOffset = ringRadius * 1.71 * Math.cos(angle);
+ const yOffset = ringRadius * Math.sin(angle);
+
+ const x = 0.5 + xOffset;
+ const y = 0.5 + yOffset;
+
+ const sizeFactor = 1.4 - ringProgress * Math.random();
+
+ coordinates.push({
+ x, y, sizeFactor,
+ });
+ }
+ }
+ return coordinates;
+ }, [clearBottomSector]);
+
useResizeObserver(containerRef, (entry) => {
setContainerSize({
width: entry.contentRect.width,
@@ -111,7 +118,7 @@ const RadialPatternBackground = ({
if (!width || !height) return;
ctx.save();
- PATTERN_POSITIONS.forEach(({
+ patternPositions.forEach(({
x, y, sizeFactor,
}) => {
const centerShift = (width - Math.max(width, MIN_SIZE * dpr)) / 2; // Shift coords if canvas is smaller than `MIN_SIZE`
diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx
index 412ad348d..fd48a668a 100644
--- a/src/components/left/main/hooks/useChatListEntry.tsx
+++ b/src/components/left/main/hooks/useChatListEntry.tsx
@@ -4,7 +4,7 @@ import React, {
import { getGlobal } from '../../../../global';
import type {
- ApiChat, ApiDraft, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser,
+ ApiChat, ApiDraft, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus,
StatefulMediaContent,
} from '../../../../api/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
@@ -21,10 +21,9 @@ import {
getMessageSticker,
getMessageVideo,
isActionMessage,
- isChatChannel,
- isChatGroup,
isExpiredMessage,
} from '../../../../global/helpers';
+import { isApiPeerChat } from '../../../../global/helpers/peers';
import { getMessageReplyInfo } from '../../../../global/helpers/replies';
import buildClassName from '../../../../util/buildClassName';
import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText';
@@ -155,15 +154,13 @@ export default function useChatListEntry({
}
if (isAction) {
- const isChat = chat && (isChatChannel(chat) || isChatGroup(chat));
-
return (
{renderActionMessageText(
oldLang,
lastMessage,
- !isChat ? lastMessageSender as ApiUser : undefined,
- isChat ? chat : undefined,
+ lastMessageSender && !isApiPeerChat(lastMessageSender) ? lastMessageSender : undefined,
+ lastMessageSender && isApiPeerChat(lastMessageSender) ? lastMessageSender : chat,
actionTargetUsers,
actionTargetMessage,
actionTargetChatId,
diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx
index 0fc926b67..35571b1c2 100644
--- a/src/components/middle/ActionMessage.tsx
+++ b/src/components/middle/ActionMessage.tsx
@@ -128,7 +128,6 @@ const ActionMessage: FC = ({
getReceipt,
openGiftInfoModalFromMessage,
openPrizeStarsTransactionFromGiveaway,
- showNotification,
} = getActions();
const oldLang = useOldLang();
@@ -240,16 +239,6 @@ const ActionMessage: FC = ({
const handleStarGiftClick = () => {
const starGift = message.content.action?.starGift;
if (!starGift) return;
- if (starGift.type === 'starGift' && starGift.canUpgrade && !message.isOutgoing) {
- showNotification({
- title: {
- key: 'ActionUnsupportedTitle',
- },
- message: {
- key: 'ActionUnsupportedDescription',
- },
- });
- }
openGiftInfoModalFromMessage({
chatId: message.chatId,
@@ -534,7 +523,8 @@ const ActionMessage: FC = ({
- {starGift.canUpgrade && !message.isOutgoing ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
+ {starGift.alreadyPaidUpgradeStars && (!message.isOutgoing || targetUsers?.[0]?.isSelf)
+ ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
{starGift.gift.availabilityTotal && (
= ({
const backgroundColors = [backdrop.centerColor, backdrop.edgeColor];
+ const adaptedPatternColor = `${backdrop.patternColor.slice(0, 7)}55`;
+
return (
@@ -571,6 +564,7 @@ const ActionMessage: FC = ({
backgroundColors={backgroundColors}
patternColor={backdrop.patternColor}
patternIcon={pattern.sticker}
+ clearBottomSector
/>
= ({
{oldLang('Gift2UniqueView')}
diff --git a/src/components/middle/message/WebPage.tsx b/src/components/middle/message/WebPage.tsx
index f5e3f6ef5..72941dddf 100644
--- a/src/components/middle/message/WebPage.tsx
+++ b/src/components/middle/message/WebPage.tsx
@@ -147,8 +147,10 @@ const WebPage: FC = ({
const isStory = type === WEBPAGE_STORY_TYPE;
const isGift = type === WEBPAGE_GIFT_TYPE;
const isExpiredStory = story && 'isDeleted' in story;
+
const quickButtonLangKey = !inPreview && !isExpiredStory ? getWebpageButtonLangKey(type) : undefined;
const quickButtonTitle = quickButtonLangKey && lang(quickButtonLangKey);
+
const truncatedDescription = trimText(description, MAX_TEXT_LENGTH);
const isArticle = Boolean(truncatedDescription || title || siteName);
let isSquarePhoto = Boolean(stickers);
@@ -185,6 +187,7 @@ const WebPage: FC = ({
size="tiny"
color="translucent"
isRectangular
+ noForcedUpperCase
onClick={handleOpenTelegramLink}
>
{caption}
diff --git a/src/components/middle/message/helpers/webpageType.ts b/src/components/middle/message/helpers/webpageType.ts
index 0ee1032bc..efe547f16 100644
--- a/src/components/middle/message/helpers/webpageType.ts
+++ b/src/components/middle/message/helpers/webpageType.ts
@@ -1,5 +1,7 @@
+import type { RegularLangKey } from '../../../../types/language';
+
// https://github.com/telegramdesktop/tdesktop/blob/3da787791f6d227f69b32bf4003bc6071d05e2ac/Telegram/SourceFiles/history/view/history_view_view_button.cpp#L51
-export function getWebpageButtonLangKey(type?: string) {
+export function getWebpageButtonLangKey(type?: string): RegularLangKey | undefined {
switch (type) {
case 'telegram_channel_request':
case 'telegram_megagroup_request':
diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx
index 6f5d9116f..47d150da7 100644
--- a/src/components/modals/ModalContainer.tsx
+++ b/src/components/modals/ModalContainer.tsx
@@ -17,6 +17,7 @@ import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.a
import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
+import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async';
import GiftCodeModal from './giftcode/GiftCodeModal.async';
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
import LocationAccessModal from './locationAccess/LocationAccessModal.async';
@@ -63,7 +64,8 @@ type ModalKey = keyof Pick;
type StateProps = {
@@ -106,6 +108,7 @@ const MODALS: ModalRegistry = {
emojiStatusAccessModal: EmojiStatusAccessModal,
locationAccessModal: LocationAccessModal,
aboutAdsModal: AboutAdsModal,
+ giftUpgradeModal: GiftUpgradeModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries;
diff --git a/src/components/modals/aboutAds/AboutAdsModal.module.scss b/src/components/modals/aboutAds/AboutAdsModal.module.scss
index 0085e9390..e514420c3 100644
--- a/src/components/modals/aboutAds/AboutAdsModal.module.scss
+++ b/src/components/modals/aboutAds/AboutAdsModal.module.scss
@@ -10,8 +10,8 @@
.moreButton {
position: absolute;
- top: 0.5rem;
- right: 0.5rem;
+ top: 0.375rem;
+ right: 0.375rem;
}
.secondary {
diff --git a/src/components/modals/aboutAds/AboutAdsModal.tsx b/src/components/modals/aboutAds/AboutAdsModal.tsx
index e64a8c72f..0360c308c 100644
--- a/src/components/modals/aboutAds/AboutAdsModal.tsx
+++ b/src/components/modals/aboutAds/AboutAdsModal.tsx
@@ -133,6 +133,7 @@ const AboutAdsModal = ({ message, minLevelToRestrictAds }: OwnProps & StateProps
isOpen={isOpen}
listItemData={modalData?.listItemData}
headerIconName="channel"
+ withSeparator
header={modalData?.header}
footer={modalData?.footer}
buttonText={oldLang('RevenueSharingAdsUnderstood')}
diff --git a/src/components/modals/common/TableAboutModal.module.scss b/src/components/modals/common/TableAboutModal.module.scss
index d9a65797d..7d9b0b148 100644
--- a/src/components/modals/common/TableAboutModal.module.scss
+++ b/src/components/modals/common/TableAboutModal.module.scss
@@ -1,5 +1,6 @@
.root :global(.modal-dialog) {
width: 26.25rem;
+ overflow: hidden;
}
.title, .description {
diff --git a/src/components/modals/common/TableAboutModal.tsx b/src/components/modals/common/TableAboutModal.tsx
index 3e9bc30bd..283d7e109 100644
--- a/src/components/modals/common/TableAboutModal.tsx
+++ b/src/components/modals/common/TableAboutModal.tsx
@@ -15,10 +15,12 @@ export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][];
type OwnProps = {
isOpen?: boolean;
listItemData?: TableAboutData;
- headerIconName: IconName;
+ headerIconName?: IconName;
header?: TeactNode;
footer?: TeactNode;
buttonText?: string;
+ hasBackdrop?: boolean;
+ withSeparator?: boolean;
onClose: NoneToVoidFunction;
onButtonClick?: NoneToVoidFunction;
};
@@ -30,6 +32,8 @@ const TableAboutModal = ({
header,
footer,
buttonText,
+ hasBackdrop,
+ withSeparator,
onClose,
onButtonClick,
}: OwnProps) => {
@@ -38,9 +42,11 @@ const TableAboutModal = ({
isOpen={isOpen}
className={styles.root}
contentClassName={styles.content}
+ hasAbsoluteCloseButton
+ absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
onClose={onClose}
>
-
+ {headerIconName &&
}
{header}
{listItemData?.map(([icon, title, subtitle]) => {
@@ -56,7 +62,7 @@ const TableAboutModal = ({
);
})}
-
+ {withSeparator && }
{footer}
{buttonText && (
diff --git a/src/components/modals/gift/GiftComposer.module.scss b/src/components/modals/gift/GiftComposer.module.scss
index d949f1ca2..92ce8f90a 100644
--- a/src/components/modals/gift/GiftComposer.module.scss
+++ b/src/components/modals/gift/GiftComposer.module.scss
@@ -1,9 +1,15 @@
+@use "../../../styles/mixins";
+
.root {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
+ overflow-x: hidden;
padding-top: 3.5rem;
+ padding-inline: 0.75rem;
+
+ @include mixins.adapt-padding-to-scrollbar(0.75rem);
}
.header {
@@ -47,9 +53,9 @@
}
.optionsSection {
- padding: 1rem;
+ padding-top: 1rem;
padding-bottom: 0.5rem;
- box-shadow: 0 1px 2px var(--color-default-shadow);
+ padding-inline: 0.25rem;
}
.checkboxTitle {
@@ -69,6 +75,9 @@
overflow: hidden;
flex: 0 0 auto;
+ border-bottom-right-radius: var(--border-radius-default);
+ border-bottom-left-radius: var(--border-radius-default);
+
background-color: var(--theme-background-color);
background-position: center;
background-repeat: no-repeat;
@@ -112,18 +121,28 @@
justify-content: space-between;
padding: 1rem;
padding-top: 0.5rem;
+ margin-inline: -0.75rem; // Account for padding
flex-grow: 1;
flex-direction: column;
background-color: var(--color-background-secondary);
+
+ position: sticky;
+ bottom: 0;
}
.switcher {
- margin-bottom: 0 !important;
+ margin-bottom: 0.25rem;
+}
+
+.switcherStarIcon {
+ margin-inline: 0 !important;
}
.description {
color: var(--color-text-secondary);
font-size: 0.875rem;
+ margin-bottom: 0.5rem;
+ margin-left: 1rem;
}
.main-button {
diff --git a/src/components/modals/gift/GiftComposer.tsx b/src/components/modals/gift/GiftComposer.tsx
index e8a2e0a32..cf0a78587 100644
--- a/src/components/modals/gift/GiftComposer.tsx
+++ b/src/components/modals/gift/GiftComposer.tsx
@@ -23,6 +23,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import PremiumProgress from '../../common/PremiumProgress';
import ActionMessage from '../../middle/ActionMessage';
import Button from '../../ui/Button';
+import Link from '../../ui/Link';
import ListItem from '../../ui/ListItem';
import Switcher from '../../ui/Switcher';
import TextArea from '../../ui/TextArea';
@@ -61,12 +62,13 @@ function GiftComposer({
currentUserId,
isPaymentFormLoading,
}: OwnProps & StateProps) {
- const { sendStarGift, openInvoice } = getActions();
+ const { sendStarGift, openInvoice, openGiftUpgradeModal } = getActions();
const lang = useLang();
const [giftMessage, setGiftMessage] = useState('');
const [shouldHideName, setShouldHideName] = useState(false);
+ const [shouldPayForUpgrade, setShouldPayForUpgrade] = useState(false);
const customBackgroundValue = useCustomBackground(theme, customBackground);
@@ -119,6 +121,7 @@ function GiftComposer({
} : undefined,
isNameHidden: shouldHideName,
starsToConvert: gift.starsToConvert,
+ canUpgrade: shouldPayForUpgrade || undefined,
isSaved: false,
gift,
},
@@ -126,7 +129,7 @@ function GiftComposer({
},
},
} satisfies ApiMessage;
- }, [currentUserId, gift, giftMessage, isStarGift, shouldHideName, userId]);
+ }, [currentUserId, gift, giftMessage, isStarGift, shouldHideName, shouldPayForUpgrade, userId]);
const handleGiftMessageChange = useLastCallback((e: ChangeEvent) => {
setGiftMessage(e.target.value);
@@ -136,6 +139,18 @@ function GiftComposer({
setShouldHideName(!shouldHideName);
});
+ const handleShouldPayForUpgradeChange = useLastCallback(() => {
+ setShouldPayForUpgrade(!shouldPayForUpgrade);
+ });
+
+ const handleOpenUpgradePreview = useLastCallback(() => {
+ if (!isStarGift) return;
+ openGiftUpgradeModal({
+ giftId: gift.id,
+ peerId: userId,
+ });
+ });
+
const handleMainButtonClick = useLastCallback(() => {
if (isStarGift) {
sendStarGift({
@@ -143,6 +158,7 @@ function GiftComposer({
shouldHideName,
gift,
message: giftMessage ? { text: giftMessage } : undefined,
+ shouldUpgrade: shouldPayForUpgrade,
});
return;
}
@@ -159,6 +175,8 @@ function GiftComposer({
function renderOptionsSection() {
const symbolsLeft = captionLimit ? captionLimit - giftMessage.length : undefined;
+
+ const userFullName = getUserFullName(user)!;
return (
+ {isStarGift && gift.upgradeStars && (
+
+
+ {lang('GiftMakeUnique', {
+ stars: formatStarsAsIcon(lang, gift.upgradeStars, { className: styles.switcherStarIcon }),
+ }, { withNodes: true })}
+
+
+
+ )}
+ {isStarGift && (
+
+ {lang('GiftMakeUniqueDescription', {
+ user: userFullName,
+ link: {lang('GiftMakeUniqueLink')},
+ }, {
+ withNodes: true,
+ })}
+
+ )}
+
{isStarGift && (
{lang('GiftHideMyName')}
@@ -180,27 +223,22 @@ function GiftComposer({
/>
)}
-
- );
- }
-
- function renderFooter() {
- const userFullName = getUserFullName(user)!;
-
- const amount = isStarGift
- ? formatStarsAsIcon(lang, gift.stars, true)
- : formatCurrency(gift.amount, gift.currency);
-
- return (
-
{isStarGift && (
{lang('GiftHideNameDescription', { profile: userFullName, receiver: userFullName })}
)}
+
+ );
+ }
-
+ function renderFooter() {
+ const amount = isStarGift
+ ? formatStarsAsIcon(lang, gift.stars + (shouldPayForUpgrade ? gift.upgradeStars! : 0), { asFont: true })
+ : formatCurrency(gift.amount, gift.currency);
+ return (
+
{isStarGift && gift.availabilityRemains && (
+
{renderOptionsSection()}
+
{renderFooter()}
);
diff --git a/src/components/modals/gift/GiftModal.tsx b/src/components/modals/gift/GiftModal.tsx
index 8a9c5bb44..135c927d9 100644
--- a/src/components/modals/gift/GiftModal.tsx
+++ b/src/components/modals/gift/GiftModal.tsx
@@ -47,7 +47,7 @@ export type GiftOption = ApiPremiumGiftCodeOption | ApiStarGiftRegular;
type StateProps = {
boostPerSentGift?: number;
starGiftsById?: Record;
- starGiftCategoriesByName: Record;
+ starGiftIdsByCategory?: Record;
starBalance?: ApiStarsAmount;
user?: ApiUser;
isSelf?: boolean;
@@ -59,7 +59,7 @@ const INTERSECTION_THROTTLE = 200;
const PremiumGiftModal: FC = ({
modal,
starGiftsById,
- starGiftCategoriesByName,
+ starGiftIdsByCategory,
starBalance,
user,
isSelf,
@@ -206,7 +206,7 @@ const PremiumGiftModal: FC = ({
function renderStarGifts() {
return (
- {starGiftsById && starGiftCategoriesByName[selectedCategory].map((giftId) => {
+ {starGiftsById && starGiftIdsByCategory?.[selectedCategory].map((giftId) => {
const gift = starGiftsById[giftId];
return (
= ({
export default memo(withGlobal((global, { modal }): StateProps => {
const {
- starGiftsById,
- starGiftCategoriesByName,
+ starGifts,
stars,
currentUserId,
} = global;
@@ -342,8 +341,8 @@ export default memo(withGlobal((global, { modal }): StateProps => {
return {
boostPerSentGift: global.appConfig?.boostsPerSentGift,
- starGiftsById,
- starGiftCategoriesByName,
+ starGiftsById: starGifts?.byId,
+ starGiftIdsByCategory: starGifts?.idsByCategory,
starBalance: stars?.balance,
user,
isSelf,
diff --git a/src/components/modals/gift/StarGiftCategoryList.tsx b/src/components/modals/gift/StarGiftCategoryList.tsx
index 1936a4d35..b4d73f0d3 100644
--- a/src/components/modals/gift/StarGiftCategoryList.tsx
+++ b/src/components/modals/gift/StarGiftCategoryList.tsx
@@ -19,22 +19,22 @@ type OwnProps = {
};
type StateProps = {
- starGiftCategoriesByName: Record;
+ idsByCategory?: Record;
};
const StarGiftCategoryList = ({
- starGiftCategoriesByName,
+ idsByCategory,
onCategoryChanged,
}: StateProps & OwnProps) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef(null);
const lang = useLang();
- const starCategories: number[] = useMemo(() => Object.keys(starGiftCategoriesByName)
+ const starCategories: number[] | undefined = useMemo(() => idsByCategory && Object.keys(idsByCategory)
.filter((category) => category !== 'all' && category !== 'limited')
.map(Number)
.sort((a, b) => a - b),
- [starGiftCategoriesByName]);
+ [idsByCategory]);
const [selectedCategory, setSelectedCategory] = useState('all');
@@ -80,17 +80,17 @@ const StarGiftCategoryList = ({
{renderCategoryItem('all')}
{renderCategoryItem('stock')}
{renderCategoryItem('limited')}
- {starCategories.map(renderCategoryItem)}
+ {starCategories?.map(renderCategoryItem)}
);
};
export default memo(withGlobal(
(global): StateProps => {
- const { starGiftCategoriesByName } = global;
+ const { starGifts } = global;
return {
- starGiftCategoriesByName,
+ idsByCategory: starGifts?.idsByCategory,
};
},
)(StarGiftCategoryList));
diff --git a/src/components/modals/gift/UniqueGiftHeader.module.scss b/src/components/modals/gift/UniqueGiftHeader.module.scss
new file mode 100644
index 000000000..17fb85bf0
--- /dev/null
+++ b/src/components/modals/gift/UniqueGiftHeader.module.scss
@@ -0,0 +1,55 @@
+.root {
+ --_height: 15rem;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: var(--_height);
+ margin-bottom: 0.5rem;
+ padding-bottom: 1rem;
+}
+
+.radialPattern {
+ position: absolute;
+ inset: -5%;
+ top: -5rem;
+ height: calc(var(--height) * 1.05);
+
+ z-index: -1;
+}
+
+.sticker {
+ margin-top: 2rem;
+}
+
+.transition {
+ position: absolute;
+ top: 0;
+ height: calc(var(--_height) + 1rem); // Account for top modal padding
+ overflow: hidden;
+}
+
+.transitionSlide {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.title {
+ font-size: 1.25rem;
+ color: white;
+ margin-top: auto;
+}
+
+.subtitle {
+ max-width: 75%;
+ font-size: 0.875rem;
+ transition: color 150ms ease-in;
+ text-align: center;
+ text-wrap: balance;
+}
+
+.title, .subtitle {
+ margin-bottom: 0;
+ z-index: 1;
+}
diff --git a/src/components/modals/gift/UniqueGiftHeader.tsx b/src/components/modals/gift/UniqueGiftHeader.tsx
new file mode 100644
index 000000000..a68aff137
--- /dev/null
+++ b/src/components/modals/gift/UniqueGiftHeader.tsx
@@ -0,0 +1,80 @@
+import React, { memo, useMemo } from '../../../lib/teact/teact';
+
+import type {
+ ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern,
+} from '../../../api/types';
+
+import buildClassName from '../../../util/buildClassName';
+import buildStyle from '../../../util/buildStyle';
+
+import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey';
+
+import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
+import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
+import Transition from '../../ui/Transition';
+
+import styles from './UniqueGiftHeader.module.scss';
+
+type OwnProps = {
+ modelAttribute: ApiStarGiftAttributeModel;
+ backdropAttribute: ApiStarGiftAttributeBackdrop;
+ patternAttribute: ApiStarGiftAttributePattern;
+ title?: string;
+ subtitle?: string;
+ className?: string;
+};
+
+const STICKER_SIZE = 120;
+
+const UniqueGiftHeader = ({
+ modelAttribute,
+ backdropAttribute,
+ patternAttribute,
+ title,
+ subtitle,
+ className,
+}: OwnProps) => {
+ const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]);
+ const subtitleColor = backdropAttribute?.textColor;
+
+ const radialPatternBackdrop = useMemo(() => {
+ const backdropColors = [backdropAttribute.centerColor, backdropAttribute.edgeColor];
+ const patternColor = backdropAttribute.patternColor;
+
+ return (
+
+ );
+ }, [backdropAttribute, patternAttribute]);
+
+ return (
+
+
+ {radialPatternBackdrop}
+
+
+ {title &&
{title}
}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+};
+
+export default memo(UniqueGiftHeader);
diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss
index a96fb4279..047af6ba8 100644
--- a/src/components/modals/gift/info/GiftInfoModal.module.scss
+++ b/src/components/modals/gift/info/GiftInfoModal.module.scss
@@ -32,11 +32,16 @@
color: var(--_color-description, var(--color-text));
}
+.footer {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
.footerDescription {
font-size: 0.875rem;
color: var(--color-text-secondary);
text-align: center;
- margin-bottom: 1rem;
}
.unknown {
@@ -82,3 +87,7 @@
font-size: 0.875rem;
}
}
+
+.starAmountIcon {
+ margin-inline-start: 0 !important;
+}
diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx
index cc2408849..b1a52e829 100644
--- a/src/components/modals/gift/info/GiftInfoModal.tsx
+++ b/src/components/modals/gift/info/GiftInfoModal.tsx
@@ -59,6 +59,7 @@ const GiftInfoModal = ({
convertGiftToStars,
openChatWithInfo,
focusMessage,
+ openGiftUpgradeModal,
} = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
@@ -81,16 +82,16 @@ const GiftInfoModal = ({
const giftSticker = gift && getStickerFromGift(gift);
const canFocusUpgrade = Boolean(userGift?.upgradeMsgId);
- const canUpdate = Boolean(userGift?.messageId) && !isSender && !canFocusUpgrade;
+ const canUpdate = Boolean(userGift?.messageId) && targetUser?.id === currentUserId && !canFocusUpgrade;
const handleClose = useLastCallback(() => {
closeGiftInfoModal();
});
const handleFocusUpgraded = useLastCallback(() => {
- if (!userGift?.upgradeMsgId) return;
- const { upgradeMsgId, fromId } = userGift;
- focusMessage({ chatId: fromId!, messageId: upgradeMsgId! });
+ if (!userGift?.upgradeMsgId || !targetUser) return;
+ const { upgradeMsgId } = userGift;
+ focusMessage({ chatId: targetUser.id, messageId: upgradeMsgId! });
handleClose();
});
@@ -107,9 +108,9 @@ const GiftInfoModal = ({
handleClose();
});
- const handleOpenProfile = useLastCallback(() => {
- openChatWithInfo({ id: currentUserId!, profileTab: 'gifts' });
- handleClose();
+ const handleOpenUpgradeModal = useLastCallback(() => {
+ if (!userGift) return;
+ openGiftUpgradeModal({ giftId: userGift.gift.id, gift: userGift });
});
const giftAttributes = useMemo(() => {
@@ -136,6 +137,30 @@ const GiftInfoModal = ({
);
}, [giftAttributes, isOpen]);
+ const renderFooterButton = useLastCallback(() => {
+ if (canFocusUpgrade) {
+ return (
+
+ );
+ }
+
+ if (canUpdate && userGift?.alreadyPaidUpgradeStars && !userGift.upgradeMsgId) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ });
+
const modalData = useMemo(() => {
if (!typeGift || !gift) {
return undefined;
@@ -154,9 +179,14 @@ const GiftInfoModal = ({
});
}
if (!userGift) return lang('GiftInfoSoldOutDescription');
+ if (userGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded');
+ if (userGift.canUpgrade && userGift.alreadyPaidUpgradeStars) {
+ return canUpdate
+ ? lang('GiftInfoDescriptionFreeUpgrade')
+ : lang('GiftInfoDescriptionFreeUpgradeOut', { user: getUserFullName(targetUser)! });
+ }
if (!canUpdate && !isSender) return undefined;
- if (!starsToConvert || canConvertDifference < 0) return undefined;
- if (isConverted) {
+ if (isConverted && starsToConvert) {
return canUpdate
? lang('GiftInfoDescriptionConverted', {
amount: formatInteger(starsToConvert!),
@@ -175,13 +205,23 @@ const GiftInfoModal = ({
});
}
+ if (userGift.canUpgrade && canUpdate) {
+ return lang('GiftInfoDescriptionUpgrade', {
+ amount: formatInteger(starsToConvert!),
+ }, {
+ pluralValue: starsToConvert!,
+ withNodes: true,
+ withMarkdown: true,
+ });
+ }
+
return canUpdate
? lang('GiftInfoDescription', {
amount: starsToConvert,
}, {
withNodes: true,
withMarkdown: true,
- pluralValue: starsToConvert,
+ pluralValue: starsToConvert || 0,
})
: lang('GiftInfoDescriptionOut', {
amount: starsToConvert,
@@ -189,7 +229,7 @@ const GiftInfoModal = ({
}, {
withNodes: true,
withMarkdown: true,
- pluralValue: starsToConvert,
+ pluralValue: starsToConvert || 0,
});
})();
@@ -269,10 +309,12 @@ const GiftInfoModal = ({
]);
}
+ const starsValue = gift.stars + (userGift?.alreadyPaidUpgradeStars || 0);
+
tableData.push([
lang('GiftInfoValue'),
- {formatStarsAsIcon(lang, gift.stars)}
+ {formatStarsAsIcon(lang, starsValue, { className: styles.starAmountIcon })}
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
@@ -296,7 +338,10 @@ const GiftInfoModal = ({
if (gift.upgradeStars) {
tableData.push([
lang('GiftInfoStatus'),
- lang('GiftInfoStatusNonUnique'),
+
+ {lang('GiftInfoStatusNonUnique')}
+ {canUpdate && {lang('GiftInfoUpgradeBadge')}}
+
,
]);
}
@@ -424,12 +469,11 @@ const GiftInfoModal = ({
{canUpdate && (
- {isUnsaved ? lang('GiftInfoHidden')
- : lang('GiftInfoSaved', {
- link: {lang('GiftInfoSavedView')},
- }, {
- withNodes: true,
- })}
+ {lang(isUnsaved ? 'GiftInfoHidden' : 'GiftInfoSaved', {
+ link: {lang('GiftInfoSavedHide')},
+ }, {
+ withNodes: true,
+ })}
{isVisibleForMe && (
@@ -438,21 +482,7 @@ const GiftInfoModal = ({
)}
)}
- {canFocusUpgrade && (
-
- )}
- {!canUpdate && !canFocusUpgrade && (
-
- )}
- {canUpdate && (
-
- )}
+ {renderFooterButton()}
);
@@ -463,7 +493,7 @@ const GiftInfoModal = ({
};
}, [
typeGift, userGift, targetUser, giftSticker, lang, canUpdate, canConvertDifference, isSender, oldLang, gift,
- radialPatternBackdrop, giftAttributes, canFocusUpgrade,
+ radialPatternBackdrop, giftAttributes, renderFooterButton,
]);
return (
diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.async.tsx b/src/components/modals/gift/upgrade/GiftUpgradeModal.async.tsx
new file mode 100644
index 000000000..ed6df5dd8
--- /dev/null
+++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.async.tsx
@@ -0,0 +1,18 @@
+import type { FC } from '../../../../lib/teact/teact';
+import React from '../../../../lib/teact/teact';
+
+import type { OwnProps } from './GiftUpgradeModal';
+
+import { Bundles } from '../../../../util/moduleLoader';
+
+import useModuleLoader from '../../../../hooks/useModuleLoader';
+
+const GiftUpgradeModalAsync: FC = (props) => {
+ const { modal } = props;
+ const GiftUpgradeModal = useModuleLoader(Bundles.Stars, 'GiftUpgradeModal', !modal);
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return GiftUpgradeModal ? : undefined;
+};
+
+export default GiftUpgradeModalAsync;
diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss b/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss
new file mode 100644
index 000000000..6cdfe382a
--- /dev/null
+++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.module.scss
@@ -0,0 +1,11 @@
+.footer {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+
+ align-self: stretch;
+}
+
+.footerButton {
+ margin-top: 0.5rem;
+}
diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx
new file mode 100644
index 000000000..e889ea16c
--- /dev/null
+++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx
@@ -0,0 +1,206 @@
+import React, {
+ memo, useEffect, useMemo, useState,
+} from '../../../../lib/teact/teact';
+import { getActions, withGlobal } from '../../../../global';
+
+import type {
+ ApiPeer,
+ ApiStarGiftAttribute,
+ ApiStarGiftAttributeBackdrop,
+ ApiStarGiftAttributeModel,
+ ApiStarGiftAttributePattern,
+ ApiStarGiftRegular,
+} from '../../../../api/types';
+import type { TabState } from '../../../../global/types';
+import { ApiMediaFormat } from '../../../../api/types';
+
+import { getPeerTitle, getStickerMediaHash } from '../../../../global/helpers';
+import { selectPeer } from '../../../../global/selectors';
+import { formatStarsAsIcon } from '../../../../util/localization/format';
+import { fetch } from '../../../../util/mediaLoader';
+
+import useInterval from '../../../../hooks/schedulers/useInterval';
+import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
+import useLang from '../../../../hooks/useLang';
+import useLastCallback from '../../../../hooks/useLastCallback';
+
+import Button from '../../../ui/Button';
+import Checkbox from '../../../ui/Checkbox';
+import TableAboutModal, { type TableAboutData } from '../../common/TableAboutModal';
+import UniqueGiftHeader from '../UniqueGiftHeader';
+
+import styles from './GiftUpgradeModal.module.scss';
+
+export type OwnProps = {
+ modal: TabState['giftUpgradeModal'];
+};
+
+type StateProps = {
+ recipient?: ApiPeer;
+};
+
+type Attributes = {
+ model: ApiStarGiftAttributeModel;
+ pattern: ApiStarGiftAttributePattern;
+ backdrop: ApiStarGiftAttributeBackdrop;
+};
+
+const PREVIEW_UPDATE_INTERVAL = 3000;
+
+const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
+ const { closeGiftUpgradeModal, upgradeGift } = getActions();
+ const isOpen = Boolean(modal);
+
+ const renderingModal = useCurrentOrPrev(modal);
+ const renderingRecipient = useCurrentOrPrev(recipient);
+ const [shouldKeepOriginalDetails, setShouldKeepOriginalDetails] = useState(false);
+
+ const [previewAttributes, setPreviewAttributes] = useState();
+
+ const lang = useLang();
+
+ const handleClose = useLastCallback(() => closeGiftUpgradeModal());
+
+ const handleUpgrade = useLastCallback(() => {
+ const gift = renderingModal?.gift;
+ if (!gift?.messageId) return;
+ upgradeGift({
+ messageId: gift.messageId,
+ shouldKeepOriginalDetails,
+ upgradeStars: !gift.alreadyPaidUpgradeStars ? (gift.gift as ApiStarGiftRegular).upgradeStars : undefined,
+ });
+ handleClose();
+ });
+
+ const updatePreviewAttributes = useLastCallback(() => {
+ if (!renderingModal?.sampleAttributes) return;
+ setPreviewAttributes(getRandomAttributes(renderingModal.sampleAttributes, previewAttributes));
+ });
+
+ useInterval(updatePreviewAttributes, isOpen ? PREVIEW_UPDATE_INTERVAL : undefined, true);
+
+ useEffect(() => {
+ if (isOpen && renderingModal?.sampleAttributes) {
+ updatePreviewAttributes();
+ }
+ }, [isOpen, renderingModal?.sampleAttributes]);
+
+ // Preload stickers and patterns
+ useEffect(() => {
+ const attributes = renderingModal?.sampleAttributes;
+ if (!attributes) return;
+ const patternStickers = attributes.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'pattern')
+ .map((attr) => attr.sticker);
+ const modelStickers = attributes.filter((attr): attr is ApiStarGiftAttributeModel => attr.type === 'model')
+ .map((attr) => attr.sticker);
+
+ const mediaHashes = [...patternStickers, ...modelStickers].map((sticker) => getStickerMediaHash(sticker, 'full'));
+ mediaHashes.forEach((hash) => {
+ fetch(hash, ApiMediaFormat.BlobUrl);
+ });
+ }, [renderingModal?.sampleAttributes]);
+
+ const modalData = useMemo(() => {
+ if (!previewAttributes) {
+ return undefined;
+ }
+
+ const gift = renderingModal?.gift;
+
+ const listItemData = [
+ ['diamond', lang('GiftUpgradeUniqueTitle'), lang('GiftUpgradeUniqueDescription')],
+ ['trade', lang('GiftUpgradeTransferableTitle'), lang('GiftUpgradeTransferableDescription')],
+ ['auction', lang('GiftUpgradeTradeableTitle'), lang('GiftUpgradeTradeableDescription')],
+ ] satisfies TableAboutData;
+
+ const subtitle = renderingRecipient
+ ? lang('GiftUpgradeText', { peer: getPeerTitle(lang, renderingRecipient) })
+ : lang('GiftUpgradeTextOwn');
+
+ const header = (
+
+ );
+
+ const footer = (
+
+ {!gift && (
+
+ )}
+ {gift && (
+ <>
+
+
+ >
+ )}
+
+ );
+
+ return {
+ listItemData,
+ header,
+ footer,
+ };
+ }, [previewAttributes, lang, renderingRecipient, renderingModal?.gift, shouldKeepOriginalDetails]);
+
+ return (
+
+ );
+};
+
+export default memo(withGlobal(
+ (global, { modal }): StateProps => {
+ const recipientId = modal?.recipientId;
+ const recipient = recipientId ? selectPeer(global, recipientId) : undefined;
+
+ return {
+ recipient,
+ };
+ },
+)(GiftUpgradeModal));
+
+function getRandomAttributes(list: ApiStarGiftAttribute[], previousSelection?: Attributes): Attributes {
+ const models = list.filter((attr): attr is ApiStarGiftAttributeModel => (
+ attr.type === 'model' && attr.name !== previousSelection?.model.name
+ ));
+ const patterns = list.filter((attr): attr is ApiStarGiftAttributePattern => (
+ attr.type === 'pattern' && attr.name !== previousSelection?.pattern.name
+ ));
+ const backdrops = list.filter((attr): attr is ApiStarGiftAttributeBackdrop => (
+ attr.type === 'backdrop' && attr.name !== previousSelection?.backdrop.name
+ ));
+
+ const randomModel = models[Math.floor(Math.random() * models.length)];
+ const randomPattern = patterns[Math.floor(Math.random() * patterns.length)];
+ const randomBackdrop = backdrops[Math.floor(Math.random() * backdrops.length)];
+
+ return {
+ model: randomModel,
+ pattern: randomPattern,
+ backdrop: randomBackdrop,
+ };
+}
diff --git a/src/components/modals/stars/StarsPaymentModal.tsx b/src/components/modals/stars/StarsPaymentModal.tsx
index 3b67e4645..c6a076835 100644
--- a/src/components/modals/stars/StarsPaymentModal.tsx
+++ b/src/components/modals/stars/StarsPaymentModal.tsx
@@ -200,7 +200,7 @@ const StarPaymentModal = ({