Unique Gift Header: Add manage buttons (#6168)

This commit is contained in:
Alexander Zinchuk 2025-09-09 20:26:11 +02:00
parent dbe78ad9e7
commit 2d238be17b
20 changed files with 615 additions and 235 deletions

View File

@ -29,13 +29,14 @@ export default tseslint.config(
quoteProps: 'as-needed', quoteProps: 'as-needed',
}), }),
globalIgnores([ globalIgnores([
'src/lib/rlottie/rlottie-wasm.js', 'src/lib/rlottie/**',
'src/lib/video-preview/polyfill', 'src/lib/video-preview/polyfill',
'src/lib/fasttextweb/fasttext-wasm.cjs', 'src/lib/fasttextweb/**',
'src/lib/gramjs/tl/', 'src/lib/gramjs/tl/',
'src/lib/lovely-chart', 'src/lib/lovely-chart/**',
'src/lib/music-metadata-browser', 'src/lib/music-metadata-browser',
'src/lib/secret-sauce/', 'src/lib/secret-sauce/',
'src/lib/fastBlur.js',
'src/types/language.d.ts', 'src/types/language.d.ts',
'dist/', 'dist/',
'dist-electron/', 'dist-electron/',
@ -56,6 +57,7 @@ export default tseslint.config(
'no-template-curly-in-string': 'error', 'no-template-curly-in-string': 'error',
'object-shorthand': 'error', 'object-shorthand': 'error',
curly: ['error', 'multi-line'], curly: ['error', 'multi-line'],
eqeqeq: ['error', 'always'],
'no-implicit-coercion': [ 'no-implicit-coercion': [
'error', 'error',
{ {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 32 32"><path d="M22.4 16.4c-3.3 0-6 2.7-6 6s2.7 6 6 6 6-2.7 6-6-2.7-6-6-6m3.2 6.8-2.2 2.2c-.4.4-1.1.4-1.6 0-.4-.4-.4-1.1 0-1.6l.4-.4h-2.6c-.6 0-1.1-.5-1.1-1.1s.5-1.1 1.1-1.1h2.6l-.3-.2c-.4-.4-.4-1.1 0-1.6.4-.4 1.1-.4 1.6 0l2.2 2.2c.4.5.4 1.2-.1 1.6M22.4 10l-2.8-6.5h4.8c.5 0 1 .3 1.3.7l4.1 5.8zm-1.3 1.6-.9 3.2c-3.3 1-5.8 4-5.8 7.7 0 .6.1 1.2.2 1.8l-3.7-12.6h10.2zM11.3 10l2.3-5.5c.3-.6.8-1 1.5-1h1.6c.6 0 1.2.4 1.5 1l2.3 5.5zm19.1 1.6L27.1 16c-1.3-.9-2.9-1.5-4.7-1.5h-.5l.8-2.9zm-21 .7L14 27.9 1.6 11.6h7.7zm.2-2.3H2.1l4.1-5.8c.4-.4.8-.7 1.4-.7h4.8z"/></svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@ -1469,7 +1469,7 @@
"GiftInfoPeerDescriptionOutConverted_one" = "{peer} converted this gift to **{amount}** Star."; "GiftInfoPeerDescriptionOutConverted_one" = "{peer} converted this gift to **{amount}** Star.";
"GiftInfoPeerDescriptionOutConverted_other" = "{peer} converted this gift to **{amount}** Stars."; "GiftInfoPeerDescriptionOutConverted_other" = "{peer} converted this gift to **{amount}** Stars.";
"GiftInfoDescriptionFreeUpgrade" = "Upgrade this gift for free to turn it to a unique collectible."; "GiftInfoDescriptionFreeUpgrade" = "Upgrade this gift for free to turn it to a unique collectible.";
"GiftInfoDescriptionUpgrade" = "Upgrade this gift to turn it to a unique collectible."; "GiftInfoDescriptionUpgrade2" = "Upgrade this gift to turn it to a unique collectible.";
"GiftInfoPeerDescriptionFreeUpgradeOut" = "{peer} can turn this gift to a unique collectible"; "GiftInfoPeerDescriptionFreeUpgradeOut" = "{peer} can turn this gift to a unique collectible";
"GiftInfoDescriptionUpgraded" = "This gift was turned into a unique collectible."; "GiftInfoDescriptionUpgraded" = "This gift was turned into a unique collectible.";
"GiftInfoFrom" = "From"; "GiftInfoFrom" = "From";

View File

@ -6,6 +6,7 @@ import type {
} from '../../../api/types'; } from '../../../api/types';
import { DEFAULT_STATUS_ICON_ID, TME_LINK_PREFIX } from '../../../config'; import { DEFAULT_STATUS_ICON_ID, TME_LINK_PREFIX } from '../../../config';
import { STARS_CURRENCY_CODE } from '../../../config';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { formatDateAtTime } from '../../../util/dates/dateFormat'; import { formatDateAtTime } from '../../../util/dates/dateFormat';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
@ -125,7 +126,7 @@ const GiftMenuItems = ({
if (!savedGift || savedGift.gift.type !== 'starGiftUnique' || !savedGift.inputGift) return; if (!savedGift || savedGift.gift.type !== 'starGiftUnique' || !savedGift.inputGift) return;
closeGiftInfoModal(); closeGiftInfoModal();
updateStarGiftPrice({ gift: savedGift.inputGift, price: { updateStarGiftPrice({ gift: savedGift.inputGift, price: {
currency: 'XTR', amount: 0, nanos: 0, currency: STARS_CURRENCY_CODE, amount: 0, nanos: 0,
} }); } });
showNotification({ showNotification({
icon: 'unlist-outline', icon: 'unlist-outline',

View File

@ -29,6 +29,7 @@ type OwnProps = {
footer?: TeactNode; footer?: TeactNode;
buttonText?: string; buttonText?: string;
className?: string; className?: string;
contentClassName?: string;
hasBackdrop?: boolean; hasBackdrop?: boolean;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onButtonClick?: NoneToVoidFunction; onButtonClick?: NoneToVoidFunction;
@ -47,6 +48,7 @@ const TableInfoModal = ({
footer, footer,
buttonText, buttonText,
className, className,
contentClassName,
hasBackdrop, hasBackdrop,
onClose, onClose,
onButtonClick, onButtonClick,
@ -70,7 +72,7 @@ const TableInfoModal = ({
header={modalHeader} header={modalHeader}
title={title} title={title}
className={className} className={className}
contentClassName={styles.content} contentClassName={buildClassName(styles.content, contentClassName)}
onClose={onClose} onClose={onClose}
withBalanceBar={withBalanceBar} withBalanceBar={withBalanceBar}
currencyInBalanceBar={currencyInBalanceBar} currencyInBalanceBar={currencyInBalanceBar}

View File

@ -9,6 +9,24 @@
height: var(--_height); height: var(--_height);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
padding-bottom: 1rem; padding-bottom: 1rem;
&.withManageButtons {
--_height: 18.5rem;
.radialPattern {
inset: -8rem -5% -5% -5%;
}
.sticker {
margin-top: 2.5rem;
}
}
:global {
canvas {
transition: 250ms transform;
}
}
} }
.subtitleBadge { .subtitleBadge {

View File

@ -4,6 +4,7 @@ import { getActions } from '../../../global';
import type { import type {
ApiPeer, ApiPeer,
ApiSavedStarGift,
ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern, ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern,
ApiTypeCurrencyAmount } from '../../../api/types'; ApiTypeCurrencyAmount } from '../../../api/types';
@ -15,7 +16,7 @@ import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle'; import buildStyle from '../../../util/buildStyle';
import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey'; import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey';
import useFlag from '../../../hooks/useFlag.ts'; import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker'; import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
@ -23,6 +24,7 @@ import Icon from '../../common/icons/Icon';
import StarIcon from '../../common/icons/StarIcon'; import StarIcon from '../../common/icons/StarIcon';
import RadialPatternBackground from '../../common/profile/RadialPatternBackground'; import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
import Transition from '../../ui/Transition'; import Transition from '../../ui/Transition';
import UniqueGiftManageButtons from './UniqueGiftManageButtons';
import styles from './UniqueGiftHeader.module.scss'; import styles from './UniqueGiftHeader.module.scss';
@ -35,6 +37,8 @@ type OwnProps = {
subtitlePeer?: ApiPeer; subtitlePeer?: ApiPeer;
className?: string; className?: string;
resellPrice?: ApiTypeCurrencyAmount; resellPrice?: ApiTypeCurrencyAmount;
showManageButtons?: boolean;
savedGift?: ApiSavedStarGift;
}; };
const STICKER_SIZE = 120; const STICKER_SIZE = 120;
@ -48,13 +52,15 @@ const UniqueGiftHeader = ({
subtitlePeer, subtitlePeer,
className, className,
resellPrice, resellPrice,
showManageButtons,
savedGift,
}: OwnProps) => { }: OwnProps) => {
const { const {
openChat, openChat,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
const [isHover, markHover, unmarkHover] = useFlag(); const [isGiftHover, markGiftHover, unmarkGiftHover] = useFlag(false);
const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]); const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]);
const subtitleColor = backdropAttribute?.textColor; const subtitleColor = backdropAttribute?.textColor;
@ -73,10 +79,14 @@ const UniqueGiftHeader = ({
}, [backdropAttribute, patternAttribute]); }, [backdropAttribute, patternAttribute]);
return ( return (
<div className={buildClassName(styles.root, className)}> <div className={buildClassName(styles.root,
isGiftHover && 'interactive-gift',
showManageButtons && styles.withManageButtons,
className)}
>
<Transition <Transition
className={styles.transition} className={styles.transition}
slideClassName={buildClassName('interactive-gift', styles.transitionSlide)} slideClassName={buildClassName(styles.transitionSlide)}
activeKey={activeKey} activeKey={activeKey}
direction={1} direction={1}
name="zoomBounceSemiFade" name="zoomBounceSemiFade"
@ -86,9 +96,9 @@ const UniqueGiftHeader = ({
className={styles.sticker} className={styles.sticker}
sticker={modelAttribute.sticker} sticker={modelAttribute.sticker}
size={STICKER_SIZE} size={STICKER_SIZE}
noLoop={!isHover} noLoop={!isGiftHover}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined} onMouseEnter={!IS_TOUCH_ENV ? markGiftHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined} onMouseLeave={!IS_TOUCH_ENV ? unmarkGiftHover : undefined}
/> />
</Transition> </Transition>
{title && <h1 className={styles.title}>{title}</h1>} {title && <h1 className={styles.title}>{title}</h1>}
@ -105,6 +115,11 @@ const UniqueGiftHeader = ({
{subtitle} {subtitle}
</div> </div>
)} )}
{savedGift && showManageButtons && (
<UniqueGiftManageButtons
savedGift={savedGift}
/>
)}
{resellPrice && ( {resellPrice && (
<p className={styles.amount}> <p className={styles.amount}>
<span> <span>

View File

@ -0,0 +1,24 @@
.manageButtons {
z-index: 1;
display: flex;
gap: 0.5rem;
width: 100%;
margin-top: 1rem;
}
.manageButton {
flex-basis: 0;
flex-grow: 1;
}
.text {
font-size: 0.875rem;
font-weight: var(--font-weight-normal);
text-transform: lowercase;
}
.icon {
font-size: 1.5rem;
}

View File

@ -0,0 +1,212 @@
import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiEmojiStatusCollectible,
ApiEmojiStatusType,
ApiSavedStarGift,
} from '../../../api/types';
import { DEFAULT_STATUS_ICON_ID } from '../../../config';
import { STARS_CURRENCY_CODE } from '../../../config';
import { selectTabState, selectUser } from '../../../global/selectors';
import { formatDateAtTime } from '../../../util/dates/dateFormat';
import { getServerTime } from '../../../util/serverTime';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Button from '../../ui/Button';
import styles from './UniqueGiftManageButtons.module.scss';
type OwnProps = {
savedGift?: ApiSavedStarGift;
};
type StateProps = {
currentUserEmojiStatus?: ApiEmojiStatusType;
collectibleEmojiStatuses?: ApiEmojiStatusType[];
};
const UniqueGiftManageButtons = ({
savedGift,
currentUserEmojiStatus,
collectibleEmojiStatuses,
}: OwnProps & StateProps) => {
const {
openGiftTransferModal,
openGiftResalePriceComposerModal,
openGiftStatusInfoModal,
setEmojiStatus,
updateStarGiftPrice,
showNotification,
closeGiftInfoModal,
} = getActions();
const lang = useLang();
const oldLang = useOldLang();
const gift = savedGift?.gift;
const isGiftUnique = gift?.type === 'starGiftUnique';
const giftResalePrice = isGiftUnique ? gift.resellPrice : undefined;
const global = getGlobal();
const modal = selectTabState(global).giftInfoModal;
const peerId = modal?.peerId;
const starGiftUniqueSlug = gift?.type === 'starGiftUnique' ? gift.slug : undefined;
const userCollectibleStatus = useMemo(() => {
if (!starGiftUniqueSlug) return undefined;
return collectibleEmojiStatuses?.find((status) =>
status.type === 'collectible' && status.slug === starGiftUniqueSlug,
) as ApiEmojiStatusCollectible | undefined;
}, [starGiftUniqueSlug, collectibleEmojiStatuses]);
const currentUniqueEmojiStatusSlug = currentUserEmojiStatus?.type === 'collectible'
? currentUserEmojiStatus.slug : undefined;
const canTakeOff = starGiftUniqueSlug !== undefined && currentUniqueEmojiStatusSlug === starGiftUniqueSlug;
const canWear = Boolean(userCollectibleStatus) && !canTakeOff;
const handleTransfer = useLastCallback(() => {
if (!savedGift || savedGift?.gift.type !== 'starGiftUnique') return;
if (savedGift.canTransferAt && savedGift.canTransferAt > getServerTime()) {
showNotification({
message: {
key: 'NotificationGiftCanTransferAt',
variables: { date: formatDateAtTime(oldLang, savedGift.canTransferAt * 1000) },
},
});
return;
}
openGiftTransferModal({ gift: savedGift });
});
const handleWear = useLastCallback(() => {
if (canTakeOff) {
setEmojiStatus({
emojiStatus: { type: 'regular', documentId: DEFAULT_STATUS_ICON_ID },
});
} else if (userCollectibleStatus) {
openGiftStatusInfoModal({ emojiStatus: userCollectibleStatus });
}
});
const handleSell = useLastCallback(() => {
if (!savedGift || !peerId) return;
if (savedGift.canResellAt && savedGift.canResellAt > getServerTime()) {
showNotification({
message: {
key: 'NotificationGiftCanResellAt',
variables: { date: formatDateAtTime(oldLang, savedGift.canResellAt * 1000) },
},
});
return;
}
openGiftResalePriceComposerModal({ peerId, gift: savedGift });
});
const handleUnlist = useLastCallback(() => {
if (!savedGift || savedGift.gift.type !== 'starGiftUnique' || !savedGift.inputGift) return;
closeGiftInfoModal();
updateStarGiftPrice({ gift: savedGift.inputGift, price: {
currency: STARS_CURRENCY_CODE, amount: 0, nanos: 0,
} });
showNotification({
icon: 'unlist-outline',
message: {
key: 'NotificationGiftIsUnlist',
variables: { gift: lang('GiftUnique', { title: savedGift.gift.title, number: savedGift.gift.number }) },
},
});
});
return (
<div className={styles.manageButtons}>
<Button
color="transparentBlured"
iconName="gift-transfer-inline"
iconAlignment="top"
iconClassName={styles.icon}
onClick={handleTransfer}
ariaLabel={lang('GiftInfoTransfer')}
noForcedUpperCase
fluid
className={styles.manageButton}
>
<span className={styles.text}>
{lang('GiftInfoTransfer')}
</span>
</Button>
{(canWear || !canTakeOff) && (
<Button
color="transparentBlured"
iconName={canTakeOff ? 'crown-take-off' : 'crown-wear'}
iconAlignment="top"
iconClassName={styles.icon}
onClick={canWear || canTakeOff ? handleWear : undefined}
disabled={!canWear && !canTakeOff}
ariaLabel={lang(canTakeOff ? 'GiftInfoTakeOff' : 'GiftInfoWear')}
noForcedUpperCase
fluid
className={styles.manageButton}
>
<span className={styles.text}>
{lang(canTakeOff ? 'GiftInfoTakeOff' : 'GiftInfoWear')}
</span>
</Button>
)}
{!giftResalePrice && (
<Button
color="transparentBlured"
iconName="sell"
iconAlignment="top"
iconClassName={styles.icon}
onClick={handleSell}
ariaLabel={lang('Sell')}
noForcedUpperCase
fluid
className={styles.manageButton}
>
<span className={styles.text}>
{lang('Sell')}
</span>
</Button>
)}
{Boolean(giftResalePrice) && (
<Button
color="transparentBlured"
iconName="unlist"
iconAlignment="top"
iconClassName={styles.icon}
onClick={handleUnlist}
ariaLabel={lang('GiftInfoUnlist')}
noForcedUpperCase
fluid
className={styles.manageButton}
>
<span className={styles.text}>
{lang('GiftInfoUnlist')}
</span>
</Button>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { currentUserId } = global;
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
const currentUserEmojiStatus = currentUser?.emojiStatus;
const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses;
return {
currentUserEmojiStatus,
collectibleEmojiStatuses,
};
},
)(UniqueGiftManageButtons));

View File

@ -36,11 +36,21 @@
color: var(--color-error); color: var(--color-error);
} }
.modalContent {
position: relative;
max-height: min(92vh, 46rem) !important;
}
.headerSplitButton { .headerSplitButton {
position: absolute; position: absolute;
right: 0.375rem; right: 0.375rem;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
border-radius: 1rem;
backdrop-filter: blur(0.5rem);
} }
.headerButton, .headerButton,

View File

@ -145,9 +145,22 @@ const GiftInfoModal = ({
return lang('GiftInfoCollectible', { number: gift.number }); return lang('GiftInfoCollectible', { number: gift.number });
}, [gift, releasedByPeer, lang]); }, [gift, releasedByPeer, lang]);
const starGiftUniqueSlug = gift?.type === 'starGiftUnique' ? gift.slug : undefined;
const selfCollectibleStatus = useMemo(() => {
if (!starGiftUniqueSlug) return undefined;
return collectibleEmojiStatuses?.find((status) =>
status.type === 'collectible' && status.slug === starGiftUniqueSlug);
}, [starGiftUniqueSlug, collectibleEmojiStatuses]);
const isSelfUnique = Boolean(selfCollectibleStatus);
const canFocusUpgrade = Boolean(savedGift?.upgradeMsgId); const canFocusUpgrade = Boolean(savedGift?.upgradeMsgId);
const canManage = !canFocusUpgrade && savedGift?.inputGift && ( const canManage = !canFocusUpgrade && savedGift?.inputGift && (
isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId isTargetChat ? hasAdminRights
: gift?.type === 'starGift'
? renderingTargetPeer?.id === currentUserId
: gift?.ownerId === currentUserId || isSelfUnique
); );
function getResalePrice(shouldPayInTon?: boolean) { function getResalePrice(shouldPayInTon?: boolean) {
@ -164,7 +177,8 @@ const GiftInfoModal = ({
const resellPrice = getResalePrice(); const resellPrice = getResalePrice();
const confirmPrice = getResalePrice(shouldPayInTon); const confirmPrice = getResalePrice(shouldPayInTon);
const canBuyGift = gift?.type === 'starGiftUnique' && gift.ownerId !== currentUserId && Boolean(resellPrice); const canBuyGift = !isSelfUnique && gift?.type === 'starGiftUnique'
&& gift.ownerId !== currentUserId && Boolean(resellPrice);
const giftOwnerTitle = (() => { const giftOwnerTitle = (() => {
if (!isGiftUnique) return undefined; if (!isGiftUnique) return undefined;
@ -277,7 +291,7 @@ const GiftInfoModal = ({
); );
} }
if (canManage && savedGift.canUpgrade && !savedGift.upgradeMsgId) { if (canManage && savedGift?.canUpgrade && !savedGift.upgradeMsgId) {
return ( return (
<Button isShiny onClick={handleOpenUpgradeModal}> <Button isShiny onClick={handleOpenUpgradeModal}>
{lang('GiftInfoUpgrade')} {lang('GiftInfoUpgrade')}
@ -327,7 +341,7 @@ const GiftInfoModal = ({
if (savedGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded'); if (savedGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded');
if (canManage && savedGift.canUpgrade && savedGift.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) { if (canManage && savedGift.canUpgrade && savedGift.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) {
return lang('GiftInfoDescriptionUpgrade'); return lang('GiftInfoDescriptionUpgrade2');
} }
if (savedGift.canUpgrade && canManage) { if (savedGift.canUpgrade && canManage) {
return canManage return canManage
@ -470,6 +484,8 @@ const GiftInfoModal = ({
title={gift.title} title={gift.title}
subtitle={giftSubtitle} subtitle={giftSubtitle}
subtitlePeer={releasedByPeer} subtitlePeer={releasedByPeer}
showManageButtons={canManage}
savedGift={savedGift}
/> />
</div> </div>
); );
@ -779,6 +795,7 @@ const GiftInfoModal = ({
tableData={modalData?.tableData} tableData={modalData?.tableData}
footer={modalData?.footer} footer={modalData?.footer}
className={styles.modal} className={styles.modal}
contentClassName={styles.modalContent}
onClose={handleClose} onClose={handleClose}
withBalanceBar={Boolean(canBuyGift)} withBalanceBar={Boolean(canBuyGift)}
currencyInBalanceBar={confirmPrice?.currency} currencyInBalanceBar={confirmPrice?.currency}

View File

@ -137,13 +137,13 @@ const StarsBalanceModal = ({
const modalHeight = useMemo(() => { const modalHeight = useMemo(() => {
const transactionCount = history?.all?.transactions.length || 0; const transactionCount = history?.all?.transactions.length || 0;
if (transactionCount == 1) { if (transactionCount === 1) {
return '35.5rem'; return '35.5rem';
} }
if (transactionCount == 2) { if (transactionCount === 2) {
return '39.25rem'; return '39.25rem';
} }
if (transactionCount == 3) { if (transactionCount === 3) {
return '43rem'; return '43rem';
} }
return '45rem'; return '45rem';

View File

@ -324,6 +324,16 @@
backdrop-filter: blur(50px); backdrop-filter: blur(50px);
} }
&.transparentBlured {
color: white;
background-color: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(0.5rem);
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
&.smaller { &.smaller {
height: 2.5rem; height: 2.5rem;
padding: 0.3125rem; padding: 0.3125rem;
@ -338,23 +348,13 @@
} }
&.with-icon { &.with-icon {
padding-right: 1.25rem; padding-inline-start: 0.75rem;
padding-left: 0.75rem; padding-inline-end: 1.25rem;
.icon { .icon {
margin-right: 0.5rem; margin-inline-end: 0.5rem;
font-size: 1.5rem; font-size: 1.5rem;
} }
&[dir="rtl"] {
padding-right: 0.75rem;
padding-left: 1.25rem;
.icon {
margin-right: 0;
margin-left: 0.5rem;
}
}
} }
} }
@ -391,6 +391,11 @@
} }
} }
&.content-with-icon-bottom,
&.content-with-icon-top {
height: auto;
}
&.fluid { &.fluid {
width: auto; width: auto;
padding-right: 1.75rem; padding-right: 1.75rem;
@ -471,4 +476,38 @@
&.rectangular { &.rectangular {
border-radius: 0; border-radius: 0;
} }
.with-icon-top {
display: flex;
flex-direction: column;
.icon {
margin-bottom: 0.25rem;
}
}
.with-icon-bottom {
display: flex;
flex-direction: column-reverse;
.icon {
margin-top: 0.25rem;
}
}
.with-icon-start {
display: flex;
.icon {
margin-inline-end: 0.25rem;
}
}
.with-icon-end {
display: flex;
flex-direction: row-reverse;
.icon {
margin-inline-start: 0.25rem;
}
}
} }

View File

@ -3,6 +3,8 @@ import type { ElementRef, FC } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact'; import type React from '../../lib/teact/teact';
import { useRef, useState } from '../../lib/teact/teact'; import { useRef, useState } from '../../lib/teact/teact';
import type { IconName } from '../../types/icons';
import { IS_TOUCH_ENV, MouseButton } from '../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV, MouseButton } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle'; import buildStyle from '../../util/buildStyle';
@ -10,6 +12,7 @@ import buildStyle from '../../util/buildStyle';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Icon from '../common/icons/Icon';
import Sparkles from '../common/Sparkles'; import Sparkles from '../common/Sparkles';
import RippleEffect from './RippleEffect'; import RippleEffect from './RippleEffect';
import Spinner from './Spinner'; import Spinner from './Spinner';
@ -23,7 +26,7 @@ export type OwnProps = {
size?: 'default' | 'smaller' | 'tiny'; size?: 'default' | 'smaller' | 'tiny';
color?: ( color?: (
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black' 'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars' | 'bluredStarsBadge' | 'translucent-bordered' | 'dark' | 'green' | 'adaptive' | 'stars' | 'bluredStarsBadge' | 'transparentBlured'
); );
backgroundImage?: string; backgroundImage?: string;
id?: string; id?: string;
@ -55,6 +58,9 @@ export type OwnProps = {
noForcedUpperCase?: boolean; noForcedUpperCase?: boolean;
shouldStopPropagation?: boolean; shouldStopPropagation?: boolean;
style?: string; style?: string;
iconName?: IconName;
iconAlignment?: 'top' | 'bottom' | 'start' | 'end';
iconClassName?: string;
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void; onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void; onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void; onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
@ -112,6 +118,9 @@ const Button: FC<OwnProps> = ({
shouldStopPropagation, shouldStopPropagation,
noForcedUpperCase, noForcedUpperCase,
style, style,
iconName,
iconAlignment = 'start',
iconClassName,
}) => { }) => {
let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>(); let elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>();
if (ref) { if (ref) {
@ -146,6 +155,7 @@ const Button: FC<OwnProps> = ({
withPremiumGradient && 'premium', withPremiumGradient && 'premium',
isRectangular && 'rectangular', isRectangular && 'rectangular',
noForcedUpperCase && 'no-upper-case', noForcedUpperCase && 'no-upper-case',
iconAlignment && iconName && `content-with-icon-${iconAlignment}`,
); );
const handleClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => { const handleClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
@ -173,15 +183,39 @@ const Button: FC<OwnProps> = ({
} }
}); });
const content = ( const renderIcon = () => {
<> if (!iconName) return undefined;
{withSparkleEffect && <Sparkles preset="button" />} return <Icon name={iconName} className={iconClassName} />;
{isLoading ? ( };
const renderContent = () => {
if (isLoading) {
return (
<div> <div>
<span dir={isRtl ? 'auto' : undefined}>{lang('Cache.ClearProgress')}</span> <span dir={isRtl ? 'auto' : undefined}>{lang('Cache.ClearProgress')}</span>
<Spinner color={isText ? 'blue' : 'white'} /> <Spinner color={isText ? 'blue' : 'white'} />
</div> </div>
) : children} );
}
const icon = renderIcon();
if (!icon) {
return children;
}
return (
<div className={`with-icon-${iconAlignment}`}>
{icon}
{children}
</div>
);
};
const content = (
<>
{withSparkleEffect && <Sparkles preset="button" />}
{renderContent()}
{!isNotInteractive && ripple && ( {!isNotInteractive && ripple && (
<RippleEffect /> <RippleEffect />
)} )}

View File

@ -133,198 +133,199 @@ $icons-map: (
"frozen-time": "\f160", "frozen-time": "\f160",
"fullscreen": "\f161", "fullscreen": "\f161",
"gifs": "\f162", "gifs": "\f162",
"gift": "\f163", "gift-transfer-inline": "\f163",
"group-filled": "\f164", "gift": "\f164",
"group": "\f165", "group-filled": "\f165",
"grouped-disable": "\f166", "group": "\f166",
"grouped": "\f167", "grouped-disable": "\f167",
"hand-stop": "\f168", "grouped": "\f168",
"hashtag": "\f169", "hand-stop": "\f169",
"hd-photo": "\f16a", "hashtag": "\f16a",
"heart-outline": "\f16b", "hd-photo": "\f16b",
"heart": "\f16c", "heart-outline": "\f16c",
"help": "\f16d", "heart": "\f16d",
"info-filled": "\f16e", "help": "\f16e",
"info": "\f16f", "info-filled": "\f16f",
"install": "\f170", "info": "\f170",
"italic": "\f171", "install": "\f171",
"key": "\f172", "italic": "\f172",
"keyboard": "\f173", "key": "\f173",
"lamp": "\f174", "keyboard": "\f174",
"language": "\f175", "lamp": "\f175",
"large-pause": "\f176", "language": "\f176",
"large-play": "\f177", "large-pause": "\f177",
"link-badge": "\f178", "large-play": "\f178",
"link-broken": "\f179", "link-badge": "\f179",
"link": "\f17a", "link-broken": "\f17a",
"location": "\f17b", "link": "\f17b",
"lock-badge": "\f17c", "location": "\f17c",
"lock": "\f17d", "lock-badge": "\f17d",
"logout": "\f17e", "lock": "\f17e",
"loop": "\f17f", "logout": "\f17f",
"mention": "\f180", "loop": "\f180",
"message-failed": "\f181", "mention": "\f181",
"message-pending": "\f182", "message-failed": "\f182",
"message-read": "\f183", "message-pending": "\f183",
"message-succeeded": "\f184", "message-read": "\f184",
"message": "\f185", "message-succeeded": "\f185",
"microphone-alt": "\f186", "message": "\f186",
"microphone": "\f187", "microphone-alt": "\f187",
"monospace": "\f188", "microphone": "\f188",
"more-circle": "\f189", "monospace": "\f189",
"more": "\f18a", "more-circle": "\f18a",
"move-caption-down": "\f18b", "more": "\f18b",
"move-caption-up": "\f18c", "move-caption-down": "\f18c",
"mute": "\f18d", "move-caption-up": "\f18d",
"muted": "\f18e", "mute": "\f18e",
"my-notes": "\f18f", "muted": "\f18f",
"new-chat-filled": "\f190", "my-notes": "\f190",
"next": "\f191", "new-chat-filled": "\f191",
"nochannel": "\f192", "next": "\f192",
"noise-suppression": "\f193", "nochannel": "\f193",
"non-contacts": "\f194", "noise-suppression": "\f194",
"one-filled": "\f195", "non-contacts": "\f195",
"open-in-new-tab": "\f196", "one-filled": "\f196",
"password-off": "\f197", "open-in-new-tab": "\f197",
"pause": "\f198", "password-off": "\f198",
"permissions": "\f199", "pause": "\f199",
"phone-discard-outline": "\f19a", "permissions": "\f19a",
"phone-discard": "\f19b", "phone-discard-outline": "\f19b",
"phone": "\f19c", "phone-discard": "\f19c",
"photo": "\f19d", "phone": "\f19d",
"pin-badge": "\f19e", "photo": "\f19e",
"pin-list": "\f19f", "pin-badge": "\f19f",
"pin": "\f1a0", "pin-list": "\f1a0",
"pinned-chat": "\f1a1", "pin": "\f1a1",
"pinned-message": "\f1a2", "pinned-chat": "\f1a2",
"pip": "\f1a3", "pinned-message": "\f1a3",
"play-story": "\f1a4", "pip": "\f1a4",
"play": "\f1a5", "play-story": "\f1a5",
"poll": "\f1a6", "play": "\f1a6",
"previous": "\f1a7", "poll": "\f1a7",
"privacy-policy": "\f1a8", "previous": "\f1a8",
"proof-of-ownership": "\f1a9", "privacy-policy": "\f1a9",
"quote-text": "\f1aa", "proof-of-ownership": "\f1aa",
"quote": "\f1ab", "quote-text": "\f1ab",
"radial-badge": "\f1ac", "quote": "\f1ac",
"rating-icons-level1": "\f1ad", "radial-badge": "\f1ad",
"rating-icons-level10": "\f1ae", "rating-icons-level1": "\f1ae",
"rating-icons-level2": "\f1af", "rating-icons-level10": "\f1af",
"rating-icons-level20": "\f1b0", "rating-icons-level2": "\f1b0",
"rating-icons-level3": "\f1b1", "rating-icons-level20": "\f1b1",
"rating-icons-level30": "\f1b2", "rating-icons-level3": "\f1b2",
"rating-icons-level4": "\f1b3", "rating-icons-level30": "\f1b3",
"rating-icons-level40": "\f1b4", "rating-icons-level4": "\f1b4",
"rating-icons-level5": "\f1b5", "rating-icons-level40": "\f1b5",
"rating-icons-level50": "\f1b6", "rating-icons-level5": "\f1b6",
"rating-icons-level6": "\f1b7", "rating-icons-level50": "\f1b7",
"rating-icons-level60": "\f1b8", "rating-icons-level6": "\f1b8",
"rating-icons-level7": "\f1b9", "rating-icons-level60": "\f1b9",
"rating-icons-level70": "\f1ba", "rating-icons-level7": "\f1ba",
"rating-icons-level8": "\f1bb", "rating-icons-level70": "\f1bb",
"rating-icons-level80": "\f1bc", "rating-icons-level8": "\f1bc",
"rating-icons-level9": "\f1bd", "rating-icons-level80": "\f1bd",
"rating-icons-level90": "\f1be", "rating-icons-level9": "\f1be",
"rating-icons-negative": "\f1bf", "rating-icons-level90": "\f1bf",
"readchats": "\f1c0", "rating-icons-negative": "\f1c0",
"recent": "\f1c1", "readchats": "\f1c1",
"refund": "\f1c2", "recent": "\f1c2",
"reload": "\f1c3", "refund": "\f1c3",
"remove-quote": "\f1c4", "reload": "\f1c4",
"remove": "\f1c5", "remove-quote": "\f1c5",
"reopen-topic": "\f1c6", "remove": "\f1c6",
"replace": "\f1c7", "reopen-topic": "\f1c7",
"replies": "\f1c8", "replace": "\f1c8",
"reply-filled": "\f1c9", "replies": "\f1c9",
"reply": "\f1ca", "reply-filled": "\f1ca",
"revenue-split": "\f1cb", "reply": "\f1cb",
"revote": "\f1cc", "revenue-split": "\f1cc",
"save-story": "\f1cd", "revote": "\f1cd",
"saved-messages": "\f1ce", "save-story": "\f1ce",
"schedule": "\f1cf", "saved-messages": "\f1cf",
"sd-photo": "\f1d0", "schedule": "\f1d0",
"search": "\f1d1", "sd-photo": "\f1d1",
"select": "\f1d2", "search": "\f1d2",
"sell-outline": "\f1d3", "select": "\f1d3",
"sell": "\f1d4", "sell-outline": "\f1d4",
"send-outline": "\f1d5", "sell": "\f1d5",
"send": "\f1d6", "send-outline": "\f1d6",
"settings-filled": "\f1d7", "send": "\f1d7",
"settings": "\f1d8", "settings-filled": "\f1d8",
"share-filled": "\f1d9", "settings": "\f1d9",
"share-screen-outlined": "\f1da", "share-filled": "\f1da",
"share-screen-stop": "\f1db", "share-screen-outlined": "\f1db",
"share-screen": "\f1dc", "share-screen-stop": "\f1dc",
"show-message": "\f1dd", "share-screen": "\f1dd",
"sidebar": "\f1de", "show-message": "\f1de",
"skip-next": "\f1df", "sidebar": "\f1df",
"skip-previous": "\f1e0", "skip-next": "\f1e0",
"smallscreen": "\f1e1", "skip-previous": "\f1e1",
"smile": "\f1e2", "smallscreen": "\f1e2",
"sort-by-date": "\f1e3", "smile": "\f1e3",
"sort-by-number": "\f1e4", "sort-by-date": "\f1e4",
"sort-by-price": "\f1e5", "sort-by-number": "\f1e5",
"sort": "\f1e6", "sort-by-price": "\f1e6",
"speaker-muted-story": "\f1e7", "sort": "\f1e7",
"speaker-outline": "\f1e8", "speaker-muted-story": "\f1e8",
"speaker-story": "\f1e9", "speaker-outline": "\f1e9",
"speaker": "\f1ea", "speaker-story": "\f1ea",
"spoiler-disable": "\f1eb", "speaker": "\f1eb",
"spoiler": "\f1ec", "spoiler-disable": "\f1ec",
"sport": "\f1ed", "spoiler": "\f1ed",
"star": "\f1ee", "sport": "\f1ee",
"stars-lock": "\f1ef", "star": "\f1ef",
"stats": "\f1f0", "stars-lock": "\f1f0",
"stealth-future": "\f1f1", "stats": "\f1f1",
"stealth-past": "\f1f2", "stealth-future": "\f1f2",
"stickers": "\f1f3", "stealth-past": "\f1f3",
"stop-raising-hand": "\f1f4", "stickers": "\f1f4",
"stop": "\f1f5", "stop-raising-hand": "\f1f5",
"story-caption": "\f1f6", "stop": "\f1f6",
"story-expired": "\f1f7", "story-caption": "\f1f7",
"story-priority": "\f1f8", "story-expired": "\f1f8",
"story-reply": "\f1f9", "story-priority": "\f1f9",
"strikethrough": "\f1fa", "story-reply": "\f1fa",
"tag-add": "\f1fb", "strikethrough": "\f1fb",
"tag-crossed": "\f1fc", "tag-add": "\f1fc",
"tag-filter": "\f1fd", "tag-crossed": "\f1fd",
"tag-name": "\f1fe", "tag-filter": "\f1fe",
"tag": "\f1ff", "tag-name": "\f1ff",
"timer": "\f200", "tag": "\f200",
"toncoin": "\f201", "timer": "\f201",
"trade": "\f202", "toncoin": "\f202",
"transcribe": "\f203", "trade": "\f203",
"truck": "\f204", "transcribe": "\f204",
"unarchive": "\f205", "truck": "\f205",
"underlined": "\f206", "unarchive": "\f206",
"understood": "\f207", "underlined": "\f207",
"unique-profile": "\f208", "understood": "\f208",
"unlist-outline": "\f209", "unique-profile": "\f209",
"unlist": "\f20a", "unlist-outline": "\f20a",
"unlock-badge": "\f20b", "unlist": "\f20b",
"unlock": "\f20c", "unlock-badge": "\f20c",
"unmute": "\f20d", "unlock": "\f20d",
"unpin": "\f20e", "unmute": "\f20e",
"unread": "\f20f", "unpin": "\f20f",
"up": "\f210", "unread": "\f210",
"user-filled": "\f211", "up": "\f211",
"user-online": "\f212", "user-filled": "\f212",
"user-stars": "\f213", "user-online": "\f213",
"user": "\f214", "user-stars": "\f214",
"video-outlined": "\f215", "user": "\f215",
"video-stop": "\f216", "video-outlined": "\f216",
"video": "\f217", "video-stop": "\f217",
"view-once": "\f218", "video": "\f218",
"voice-chat": "\f219", "view-once": "\f219",
"volume-1": "\f21a", "voice-chat": "\f21a",
"volume-2": "\f21b", "volume-1": "\f21b",
"volume-3": "\f21c", "volume-2": "\f21c",
"warning": "\f21d", "volume-3": "\f21d",
"web": "\f21e", "warning": "\f21e",
"webapp": "\f21f", "web": "\f21f",
"word-wrap": "\f220", "webapp": "\f220",
"zoom-in": "\f221", "word-wrap": "\f221",
"zoom-out": "\f222", "zoom-in": "\f222",
"zoom-out": "\f223",
); );
.icon-active-sessions::before { .icon-active-sessions::before {
@ -621,6 +622,9 @@ $icons-map: (
.icon-gifs::before { .icon-gifs::before {
content: map.get($icons-map, "gifs"); content: map.get($icons-map, "gifs");
} }
.icon-gift-transfer-inline::before {
content: map.get($icons-map, "gift-transfer-inline");
}
.icon-gift::before { .icon-gift::before {
content: map.get($icons-map, "gift"); content: map.get($icons-map, "gift");
} }

Binary file not shown.

Binary file not shown.

View File

@ -97,6 +97,7 @@ export type FontIconName =
| 'frozen-time' | 'frozen-time'
| 'fullscreen' | 'fullscreen'
| 'gifs' | 'gifs'
| 'gift-transfer-inline'
| 'gift' | 'gift'
| 'group-filled' | 'group-filled'
| 'group' | 'group'

View File

@ -1230,7 +1230,7 @@ export interface LangPair {
'GiftInfoDescriptionRegular': undefined; 'GiftInfoDescriptionRegular': undefined;
'GiftInfoDescriptionUpgradeRegular': undefined; 'GiftInfoDescriptionUpgradeRegular': undefined;
'GiftInfoDescriptionFreeUpgrade': undefined; 'GiftInfoDescriptionFreeUpgrade': undefined;
'GiftInfoDescriptionUpgrade': undefined; 'GiftInfoDescriptionUpgrade2': undefined;
'GiftInfoDescriptionUpgraded': undefined; 'GiftInfoDescriptionUpgraded': undefined;
'GiftInfoFrom': undefined; 'GiftInfoFrom': undefined;
'GiftInfoDate': undefined; 'GiftInfoDate': undefined;

View File

@ -26,7 +26,7 @@ export function rgb2hex(param: [number, number, number]) {
const p0 = param[0].toString(16); const p0 = param[0].toString(16);
const p1 = param[1].toString(16); const p1 = param[1].toString(16);
const p2 = param[2].toString(16); const p2 = param[2].toString(16);
return (p0.length == 1 ? '0' + p0 : p0) + (p1.length == 1 ? '0' + p1 : p1) + (p2.length == 1 ? '0' + p2 : p2); return (p0.length === 1 ? '0' + p0 : p0) + (p1.length === 1 ? '0' + p1 : p1) + (p2.length === 1 ? '0' + p2 : p2);
} }
/** /**
@ -49,9 +49,9 @@ export function rgb2hsb([r, g, b]: [number, number, number]): [number, number, n
let h!: number, s: number, v: number = max; let h!: number, s: number, v: number = max;
let d = max - min; let d = max - min;
s = max == 0 ? 0 : d / max; s = max === 0 ? 0 : d / max;
if (max == min) { if (max === min) {
h = 0; // achromatic h = 0; // achromatic
} else { } else {
switch (max) { switch (max) {