From b40fe83338f124cfa1da233fb061dbda927f50c1 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 22 Feb 2026 23:42:48 +0100 Subject: [PATCH] Gift Modal: Fix UI (#6642) --- .../modals/gift/GiftModal.module.scss | 7 +- src/components/modals/gift/GiftModal.tsx | 41 +++++----- .../modals/gift/GiftResaleFilters.module.scss | 79 +++++++++++++++++-- .../modals/gift/GiftResaleFilters.tsx | 73 ++++++++++++----- src/components/ui/Modal.scss | 4 +- src/components/ui/Modal.tsx | 12 +-- .../ui/ModalStarBalanceBar.module.scss | 25 +++--- src/components/ui/ModalStarBalanceBar.tsx | 11 +-- .../element/useIsTopmostBalanceBarModal.ts | 59 ++++++++++++++ 9 files changed, 241 insertions(+), 70 deletions(-) create mode 100644 src/hooks/element/useIsTopmostBalanceBarModal.ts diff --git a/src/components/modals/gift/GiftModal.module.scss b/src/components/modals/gift/GiftModal.module.scss index 121b7af9e..6c4a468a0 100644 --- a/src/components/modals/gift/GiftModal.module.scss +++ b/src/components/modals/gift/GiftModal.module.scss @@ -133,7 +133,6 @@ .resaleHeaderContentContainer { align-items: center; - justify-items: center; width: 100%; } @@ -166,6 +165,12 @@ .resaleHeaderText { margin: 0; margin-bottom: 0.0625rem !important; + font-size: 1rem; + line-height: 1.3125rem; +} + +.resaleHeaderTextBlock { + margin-left: 4.5rem; } .resaleHeaderDescription { diff --git a/src/components/modals/gift/GiftModal.tsx b/src/components/modals/gift/GiftModal.tsx index 92cefb906..0e9bfcfac 100644 --- a/src/components/modals/gift/GiftModal.tsx +++ b/src/components/modals/gift/GiftModal.tsx @@ -40,7 +40,6 @@ import Button from '../../ui/Button'; import InfiniteScroll from '../../ui/InfiniteScroll'; import Modal from '../../ui/Modal'; import Transition from '../../ui/Transition'; -import BalanceBlock from '../stars/BalanceBlock'; import GiftComposer from './GiftComposer'; import GiftItemPremium from './GiftItemPremium'; import GiftItemStar from './GiftItemStar'; @@ -518,23 +517,25 @@ const GiftModal: FC = ({ const isFirstLoading = areResaleGiftsLoading && !resaleGiftsCount; return (
-

- {selectedResaleGift.title} -

- {isFirstLoading - && ( -
- {lang('Loading')} -
- )} - {!isFirstLoading && resaleGiftsCount !== undefined - && ( -
- {lang('HeaderDescriptionResaleGifts', { - count: resaleGiftsCount, - }, { withNodes: true, withMarkdown: true, pluralValue: resaleGiftsCount })} -
- )} +
+

+ {selectedResaleGift.title} +

+ {isFirstLoading + && ( +
+ {lang('Loading')} +
+ )} + {!isFirstLoading && resaleGiftsCount !== undefined + && ( +
+ {lang('HeaderDescriptionResaleGifts', { + count: resaleGiftsCount, + }, { withNodes: true, withMarkdown: true, pluralValue: resaleGiftsCount })} +
+ )} +
); @@ -555,6 +556,7 @@ const GiftModal: FC = ({ contentClassName={styles.content} className={buildClassName(styles.modalDialog, styles.root)} isLowStackPriority + withBalanceBar > -
= ({ }; }, [filteredAttributes, searchModelQuery, searchBackdropQuery, searchPatternQuery]); + const countersMap = useMemo(() => { + const map = { + model: new Map(), + pattern: new Map(), + backdrop: new Map(), + }; + + for (const counter of counters ?? []) { + const { attribute, count } = counter; + if (attribute.type === 'model') { + map.model.set(attribute.documentId, count); + } else if (attribute.type === 'pattern') { + map.pattern.set(attribute.documentId, count); + } else if (attribute.type === 'backdrop') { + map.backdrop.set(attribute.backdropId, count); + } + } + + return map; + }, [counters]); + // Sort Menu const sortMenuRef = useRef(); const { @@ -175,18 +196,21 @@ const GiftResaleFilters: FC = ({ const SortMenuButton: FC<{ onTrigger: (e: ReactMouseEvent) => void; isOpen?: boolean }> = useMemo(() => { const sortType = filter.sortType; + const iconName = sortType === 'byDate' ? 'sort-by-date' + : sortType === 'byNumber' ? 'sort-by-number' + : 'sort-by-price'; return ({ onTrigger, isOpen: isMenuOpen }) => (
+ {sortType === 'byDate' && lang('ValueGiftSortByDate')} {sortType === 'byNumber' && lang('ValueGiftSortByNumber')} {sortType === 'byPrice' && lang('ValueGiftSortByPrice')} -
); }, [lang, filter]); @@ -203,10 +227,7 @@ const GiftResaleFilters: FC = ({ {attributesCount === 0 && lang('GiftAttributeModel')} {attributesCount > 0 && lang('GiftAttributeModelPlural', { count: attributesCount }, { pluralValue: attributesCount })} - + {renderDropdownArrows(isMenuOpen)}
); }, [lang, filter]); @@ -222,10 +243,7 @@ const GiftResaleFilters: FC = ({ {attributesCount === 0 && lang('GiftAttributeBackdrop')} {attributesCount > 0 && lang('GiftAttributeBackdropPlural', { count: attributesCount }, { pluralValue: attributesCount })} - + {renderDropdownArrows(isMenuOpen)} ); }, [lang, filter]); @@ -240,10 +258,7 @@ const GiftResaleFilters: FC = ({ {attributesCount === 0 && lang('GiftAttributeSymbol')} {attributesCount > 0 && lang('GiftAttributeSymbolPlural', { count: attributesCount }, { pluralValue: attributesCount })} - + {renderDropdownArrows(isMenuOpen)} ); }, [lang, filter]); @@ -334,6 +349,17 @@ const GiftResaleFilters: FC = ({ } }); }); + function renderDropdownArrows(isOpen?: boolean) { + return ( +
+
+
+
+
+
+ ); + } + function renderSortMenuItems() { return ( <> @@ -425,7 +451,7 @@ const GiftResaleFilters: FC = ({ onReset={handleSearchModelInputReset} placeholder={lang('Search')} /> - + {lang('ContextMenuItemSelectAll')} {models.map((model) => { @@ -447,6 +473,9 @@ const GiftResaleFilters: FC = ({ />
{model.name} + + {lang.number(countersMap.model.get(model.sticker.id) || 0)} +
= ({ onReset={handleSearchBackdropInputReset} placeholder={lang('Search')} /> - + {lang('ContextMenuItemSelectAll')} {backdrops.map((backdrop) => { @@ -515,6 +544,9 @@ const GiftResaleFilters: FC = ({ />
{backdrop.name} + + {lang.number(countersMap.backdrop.get(backdrop.backdropId) || 0)} +
= ({ onReset={handleSearchPatternInputReset} placeholder={lang('Search')} /> - + {lang('ContextMenuItemSelectAll')} {patterns.map((pattern) => { @@ -583,6 +615,9 @@ const GiftResaleFilters: FC = ({
{pattern.name} + + {lang.number(countersMap.pattern.get(pattern.sticker.id) || 0)} +
= ({ tabIndex={-1} role="dialog" > - {withBalanceBar && ( - - )}
+ {withBalanceBar && ( + + )}
{renderHeader()} {Boolean(moreMenuItems) && ( diff --git a/src/components/ui/ModalStarBalanceBar.module.scss b/src/components/ui/ModalStarBalanceBar.module.scss index d6753510a..88eff7650 100644 --- a/src/components/ui/ModalStarBalanceBar.module.scss +++ b/src/components/ui/ModalStarBalanceBar.module.scss @@ -1,14 +1,12 @@ .root { - position: absolute; z-index: var(--z-modal); - top: 0rem; - left: 50%; - transform: translate(-50%, -1rem); display: flex; flex-direction: column; align-items: center; + margin-top: 2rem; + margin-bottom: 1rem; padding: 0.5rem 1.25rem; border-radius: 2rem; @@ -20,6 +18,12 @@ transition: transform 0.2s ease, opacity 0.2s ease; + &.hidden { + pointer-events: none; + transform: scale(0); + opacity: 0; + } + :global(.confirm) & { z-index: var(--z-modal-confirm); } @@ -32,14 +36,6 @@ transform: none !important; transition: none; } - - &:not(:global(.open)) { - transform: translate(-50%, 0); - } - - &:not(:global(.closing)) { - transform: translate(-50%, 1rem); - } } .starIcon { @@ -51,3 +47,8 @@ .tonInUsdDescription { color: var(--color-text-secondary); } + +.tonInUsdDescription, +.getMoreStarsLink { + font-weight: var(--font-weight-semibold); +} diff --git a/src/components/ui/ModalStarBalanceBar.tsx b/src/components/ui/ModalStarBalanceBar.tsx index 0de63bdf3..2f916ca0b 100644 --- a/src/components/ui/ModalStarBalanceBar.tsx +++ b/src/components/ui/ModalStarBalanceBar.tsx @@ -1,6 +1,4 @@ -import { - memo, -} from '../../lib/teact/teact'; +import { memo } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { ApiStarsAmount, ApiTonAmount } from '../../api/types'; @@ -10,6 +8,7 @@ import buildClassName from '../../util/buildClassName'; import { convertTonFromNanos, convertTonToUsd, formatCurrencyAsString } from '../../util/formatCurrency'; import { formatStarsAsIcon, formatTonAsIcon } from '../../util/localization/format'; +import useIsTopmostBalanceBarModal from '../../hooks/element/useIsTopmostBalanceBarModal'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useShowTransition from '../../hooks/useShowTransition'; @@ -56,6 +55,8 @@ function ModalStarBalanceBar({ withShouldRender: true, }); + const isTopmost = useIsTopmostBalanceBarModal(ref, Boolean(shouldRender && currentBalance)); + const handleGetMoreStars = useLastCallback(() => { openStarsBalanceModal(isTonMode ? { currency: 'TON' } : {}); }); @@ -66,7 +67,7 @@ function ModalStarBalanceBar({ return (
@@ -101,7 +102,7 @@ function ModalStarBalanceBar({
)} {!isTonMode && ( - + {lang('GetMoreStarsLinkText')} )} diff --git a/src/hooks/element/useIsTopmostBalanceBarModal.ts b/src/hooks/element/useIsTopmostBalanceBarModal.ts new file mode 100644 index 000000000..e2a55c675 --- /dev/null +++ b/src/hooks/element/useIsTopmostBalanceBarModal.ts @@ -0,0 +1,59 @@ +import { useEffect, useState } from '../../lib/teact/teact'; + +import { createCallbackManager } from '../../util/callbacks'; + +const BALANCE_BAR_MODAL_SELECTOR = '.Modal.with-balance-bar'; + +const balanceBarCallbacks = createCallbackManager(); + +export default function useIsTopmostBalanceBarModal( + ref: { current?: HTMLElement }, + isActive?: boolean, +) { + const [isTopmost, setIsTopmost] = useState(true); + + useEffect(() => { + if (!isActive) return undefined; + + const updateIsTopmost = () => { + setIsTopmost(checkIsTopmostBalanceBarModal(ref.current)); + }; + + updateIsTopmost(); + + const unsubscribe = balanceBarCallbacks.addCallback(updateIsTopmost); + balanceBarCallbacks.runCallbacks(); + + return () => { + unsubscribe(); + balanceBarCallbacks.runCallbacks(); + }; + }, [isActive, ref]); + + return isTopmost; +} + +function checkIsTopmostBalanceBarModal(element?: HTMLElement) { + if (!element) return true; + + const parentModal = element.closest(BALANCE_BAR_MODAL_SELECTOR); + if (!parentModal) return true; + + const allBalanceBarModals = document.querySelectorAll(BALANCE_BAR_MODAL_SELECTOR); + if (allBalanceBarModals.length <= 1) { + return true; + } + + let topmostModal: Element | undefined; + let highestZIndex = -Infinity; + + allBalanceBarModals.forEach((modal) => { + const zIndex = parseInt(getComputedStyle(modal).zIndex, 10) || 0; + if (zIndex >= highestZIndex) { + highestZIndex = zIndex; + topmostModal = modal; + } + }); + + return parentModal === topmostModal; +}