From 33486a09d01b19961e02d4d72b63710e7cf8bc8f Mon Sep 17 00:00:00 2001 From: Fringg Date: Fri, 20 Mar 2026 04:23:45 +0300 Subject: [PATCH] fix: adapt referral network for Telegram MiniApp safe areas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract useHeaderHeight() hook — single source of truth for header height across AppShell, ReferralNetwork, and TicketNotificationBell - Portal top offset now computed dynamically from TG safe area insets instead of hardcoded top-16 (fixes overlap with TG header bar) - Two-row mobile layout: title + selector on separate rows so dropdown gets full width on small screens - Add MOBILE_HEADER_HEIGHT_PX and DESKTOP_HEADER_HEIGHT_PX to constants - Fix TicketNotificationBell using hardcoded 45px for all platforms --- src/components/TicketNotificationBell.tsx | 15 +++-------- src/components/layout/AppShell/AppShell.tsx | 12 +++------ src/config/constants.ts | 2 ++ src/hooks/useHeaderHeight.ts | 25 +++++++++++++++++++ src/pages/ReferralNetwork/ReferralNetwork.tsx | 19 +++++++++----- 5 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 src/hooks/useHeaderHeight.ts diff --git a/src/components/TicketNotificationBell.tsx b/src/components/TicketNotificationBell.tsx index 24018a8..e840bc9 100644 --- a/src/components/TicketNotificationBell.tsx +++ b/src/components/TicketNotificationBell.tsx @@ -6,7 +6,7 @@ import { ticketNotificationsApi } from '../api/ticketNotifications'; import { useAuthStore } from '../store/auth'; import { useToast } from './Toast'; import { useWebSocket, WSMessage } from '../hooks/useWebSocket'; -import { useTelegramSDK } from '../hooks/useTelegramSDK'; +import { useHeaderHeight } from '../hooks/useHeaderHeight'; import type { TicketNotification } from '../types'; const BellIcon = () => ( @@ -37,12 +37,7 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi const { showToast } = useToast(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); - const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramSDK(); - - // Calculate dropdown top position (account for fullscreen safe area + TG buttons) - const dropdownTop = isFullscreen - ? Math.max(safeAreaInset.top, contentSafeAreaInset.top) + 45 + 64 // safe area + TG buttons + header - : 64; // default header height + const { mobile: dropdownTop } = useHeaderHeight(); // Show toast for WebSocket notification const showWSNotificationToast = useCallback( @@ -259,10 +254,8 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi {/* Dropdown */} {isOpen && (
{/* Header */}
diff --git a/src/components/layout/AppShell/AppShell.tsx b/src/components/layout/AppShell/AppShell.tsx index 80abd12..9441ed5 100644 --- a/src/components/layout/AppShell/AppShell.tsx +++ b/src/components/layout/AppShell/AppShell.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/store/auth'; import { useHaptic } from '@/platform'; import { useTelegramSDK } from '@/hooks/useTelegramSDK'; +import { useHeaderHeight } from '@/hooks/useHeaderHeight'; import { useTheme } from '@/hooks/useTheme'; import { useBranding } from '@/hooks/useBranding'; import { useFeatureFlags } from '@/hooks/useFeatureFlags'; @@ -13,7 +14,6 @@ import { useScrollRestoration } from '@/hooks/useScrollRestoration'; import { themeColorsApi } from '@/api/themeColors'; import { isLogoPreloaded } from '@/api/branding'; import { cn } from '@/lib/utils'; -import { UI } from '@/config/constants'; import WebSocketNotifications from '@/components/WebSocketNotifications'; import CampaignBonusNotifier from '@/components/CampaignBonusNotifier'; @@ -198,6 +198,7 @@ export function AppShell({ children }: AppShellProps) { const logout = useAuthStore((state) => state.logout); const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } = useTelegramSDK(); + const { mobile: headerHeight } = useHeaderHeight(); const haptic = useHaptic(); const { toggleTheme, isDark } = useTheme(); @@ -269,14 +270,7 @@ export function AppShell({ children }: AppShellProps) { haptic.impact('light'); }; - // Calculate header height based on fullscreen mode (only on mobile Telegram) - // On iOS: contentSafeAreaInset.top includes status bar + dynamic island + Telegram header - // On Android: safeAreaInset.top only includes status bar, need to add Telegram header height (~48px) - const telegramHeaderHeight = - platform === 'android' ? UI.TELEGRAM_HEADER_ANDROID_PX : UI.TELEGRAM_HEADER_IOS_PX; - const headerHeight = isMobileFullscreen - ? 64 + Math.max(safeAreaInset.top, contentSafeAreaInset.top) + telegramHeaderHeight - : 64; + // headerHeight comes from useHeaderHeight() — accounts for TG safe area in fullscreen return (
diff --git a/src/config/constants.ts b/src/config/constants.ts index 2b8f754..21e4f29 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -17,6 +17,8 @@ export const UI = { RESEND_COOLDOWN_SEC: 60, TELEGRAM_HEADER_ANDROID_PX: 48, TELEGRAM_HEADER_IOS_PX: 45, + MOBILE_HEADER_HEIGHT_PX: 64, + DESKTOP_HEADER_HEIGHT_PX: 56, } as const; // API diff --git a/src/hooks/useHeaderHeight.ts b/src/hooks/useHeaderHeight.ts new file mode 100644 index 0000000..8b1b0d6 --- /dev/null +++ b/src/hooks/useHeaderHeight.ts @@ -0,0 +1,25 @@ +import { useTelegramSDK } from '@/hooks/useTelegramSDK'; +import { UI } from '@/config/constants'; + +/** + * Computes the app header height in pixels, accounting for + * Telegram MiniApp safe area insets in fullscreen mode. + * + * Desktop: 56px (h-14). Mobile: 64px (h-16) + safe area + TG header when fullscreen. + */ +export function useHeaderHeight(): { mobile: number; desktop: number } { + const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } = + useTelegramSDK(); + const isMobileFullscreen = isFullscreen && isMobile; + + const telegramHeaderHeight = + platform === 'android' ? UI.TELEGRAM_HEADER_ANDROID_PX : UI.TELEGRAM_HEADER_IOS_PX; + + const mobile = isMobileFullscreen + ? UI.MOBILE_HEADER_HEIGHT_PX + + Math.max(safeAreaInset.top, contentSafeAreaInset.top) + + telegramHeaderHeight + : UI.MOBILE_HEADER_HEIGHT_PX; + + return { mobile, desktop: UI.DESKTOP_HEADER_HEIGHT_PX }; +} diff --git a/src/pages/ReferralNetwork/ReferralNetwork.tsx b/src/pages/ReferralNetwork/ReferralNetwork.tsx index 4e08d27..5fbe7dc 100644 --- a/src/pages/ReferralNetwork/ReferralNetwork.tsx +++ b/src/pages/ReferralNetwork/ReferralNetwork.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { referralNetworkApi } from '@/api/referralNetwork'; import { useReferralNetworkStore } from '@/store/referralNetwork'; +import { useHeaderHeight } from '@/hooks/useHeaderHeight'; import { AdminBackButton } from '@/components/admin/AdminBackButton'; import { NetworkGraph } from './components/NetworkGraph'; import { ScopeSelector } from './components/ScopeSelector'; @@ -20,6 +21,8 @@ export function ReferralNetwork() { const removeScope = useReferralNetworkStore((s) => s.removeScope); const clearScope = useReferralNetworkStore((s) => s.clearScope); + const { mobile: mobileHeaderHeight } = useHeaderHeight(); + const hasScope = scope.length > 0; const { @@ -38,14 +41,18 @@ export function ReferralNetwork() { return createPortal(
-
- -

- {t('admin.referralNetwork.title')} -

+ {/* Mobile: two rows — title on top, selector below */} +
+
+ +

+ {t('admin.referralNetwork.title')} +

+