/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import React from 'react'; import {clearPendingBulkDeletionNagbarDismissal} from '~/actions/NagbarActionCreators'; import AppStorage from '~/lib/AppStorage'; import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore'; import NagbarStore from '~/stores/NagbarStore'; import UserStore from '~/stores/UserStore'; import {isDesktop} from '~/utils/NativeUtils'; import * as NotificationUtils from '~/utils/NotificationUtils'; import {isStandalonePwa} from '~/utils/PwaUtils'; import {type AppLayoutState, type NagbarConditions, type NagbarState, NagbarType, UPDATE_DISMISS_KEY} from './types'; export const useAppLayoutState = (): AppLayoutState => { const [isStandalone, setIsStandalone] = React.useState(isStandalonePwa()); React.useEffect(() => { const checkStandalone = () => { setIsStandalone(isStandalonePwa()); }; checkStandalone(); document.documentElement.classList.toggle('is-standalone', isStandalone); return () => { document.documentElement.classList.remove('is-standalone'); }; }, [isStandalone]); return {isStandalone}; }; export const useNagbarConditions = (): NagbarConditions => { const user = UserStore.currentUser; const nagbarState = NagbarStore; const premiumOverrideType = DeveloperOptionsStore.premiumTypeOverride; const premiumWillCancel = user?.premiumWillCancel ?? false; const isMockPremium = premiumOverrideType != null && premiumOverrideType > 0; const previousPendingBulkDeletionKeyRef = React.useRef(null); React.useEffect(() => { const handleVersionUpdateAvailable = () => { if (nagbarState.forceUpdateAvailable) { return; } const dismissedUntilStr = AppStorage.getItem(UPDATE_DISMISS_KEY); if (dismissedUntilStr) { const dismissedUntil = Number.parseInt(dismissedUntilStr, 10); if (Date.now() < dismissedUntil) { return; } localStorage.removeItem(UPDATE_DISMISS_KEY); } }; window.addEventListener('version-update-available', handleVersionUpdateAvailable as EventListener); return () => { window.removeEventListener('version-update-available', handleVersionUpdateAvailable as EventListener); }; }, [nagbarState.forceUpdateAvailable]); const shouldShowDesktopNotification = (() => { if (!NotificationUtils.hasNotification()) return false; if (typeof Notification !== 'undefined') { if (Notification.permission === 'granted') return false; if (Notification.permission === 'denied') return false; } return true; })(); const canShowPremiumGracePeriod = (() => { if (nagbarState.forceHidePremiumGracePeriod) return false; if (nagbarState.forcePremiumGracePeriod) return true; if (!user?.premiumUntil || user.premiumType === 2 || premiumWillCancel) return false; const now = new Date(); const expiryDate = new Date(user.premiumUntil); const gracePeriodMs = 3 * 24 * 60 * 60 * 1000; const graceEndDate = new Date(expiryDate.getTime() + gracePeriodMs); const isInGracePeriod = now > expiryDate && now <= graceEndDate; return isInGracePeriod; })(); const canShowPremiumExpired = (() => { if (nagbarState.forceHidePremiumExpired) return false; if (nagbarState.forcePremiumExpired) return true; if (!user?.premiumUntil || user.premiumType === 2 || premiumWillCancel) return false; const now = new Date(); const expiryDate = new Date(user.premiumUntil); const gracePeriodMs = 3 * 24 * 60 * 60 * 1000; const expiredStateDurationMs = 30 * 24 * 60 * 60 * 1000; const graceEndDate = new Date(expiryDate.getTime() + gracePeriodMs); const expiredStateEndDate = new Date(expiryDate.getTime() + expiredStateDurationMs); const isExpired = now > graceEndDate; const showExpiredState = isExpired && now <= expiredStateEndDate; return showExpiredState; })(); const canShowGiftInventory = (() => { if (nagbarState.forceHideGiftInventory) return false; if (nagbarState.forceGiftInventory) return true; return Boolean(user?.hasUnreadGiftInventory && !nagbarState.giftInventoryDismissed); })(); const canShowPremiumOnboarding = (() => { if (nagbarState.forceHidePremiumOnboarding) return false; if (nagbarState.forcePremiumOnboarding) return true; if (isMockPremium) return false; return Boolean( user?.isPremium() && !user?.hasDismissedPremiumOnboarding && !nagbarState.premiumOnboardingDismissed, ); })(); const isNativeDesktop = isDesktop(); const isMobileDevice = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); const isDesktopBrowser = !isNativeDesktop && !isMobileDevice; const canShowDesktopDownload = (() => { if (nagbarState.forceHideDesktopDownload) return false; if (nagbarState.forceDesktopDownload) return true; return isDesktopBrowser && !nagbarState.desktopDownloadDismissed; })(); const canShowMobileDownload = (() => { if (nagbarState.forceHideMobileDownload) return false; if (nagbarState.forceMobileDownload) return true; return Boolean(!isMobileDevice && user?.usedMobileClient === false && !nagbarState.mobileDownloadDismissed); })(); const pendingBulkDeletion = user?.getPendingBulkMessageDeletion(); const pendingBulkDeletionKey = pendingBulkDeletion?.scheduledAt.toISOString() ?? null; React.useEffect(() => { const previousKey = previousPendingBulkDeletionKeyRef.current; if (previousKey && previousKey !== pendingBulkDeletionKey) { clearPendingBulkDeletionNagbarDismissal(previousKey); } previousPendingBulkDeletionKeyRef.current = pendingBulkDeletionKey; }, [pendingBulkDeletionKey]); const hasPendingBulkMessageDeletion = Boolean( pendingBulkDeletion && !nagbarState.hasPendingBulkDeletionDismissed(pendingBulkDeletionKey), ); const canShowGuildMembershipCta = (() => { if (nagbarState.forceHideGuildMembershipCta) return false; if (nagbarState.forceGuildMembershipCta) return true; if (!user) return false; return !nagbarState.guildMembershipCtaDismissed; })(); return { userIsUnclaimed: nagbarState.forceHideUnclaimedAccount ? false : nagbarState.forceUnclaimedAccount ? true : Boolean(user && !user.isClaimed()), userNeedsVerification: nagbarState.forceHideEmailVerification ? false : nagbarState.forceEmailVerification ? true : Boolean(user?.isClaimed() && !user.verified), canShowDesktopNotification: nagbarState.forceHideDesktopNotification ? false : nagbarState.forceDesktopNotification ? true : shouldShowDesktopNotification && !nagbarState.desktopNotificationDismissed, canShowPremiumGracePeriod, canShowPremiumExpired, canShowPremiumOnboarding, canShowGiftInventory, canShowDesktopDownload, canShowMobileDownload, hasPendingBulkMessageDeletion, canShowGuildMembershipCta, }; }; export const useActiveNagbars = (conditions: NagbarConditions): Array => { return React.useMemo(() => { const undismissibleTypes = new Set([ NagbarType.UNCLAIMED_ACCOUNT, NagbarType.EMAIL_VERIFICATION, NagbarType.BULK_DELETE_PENDING, ]); const nagbars: Array = [ { type: NagbarType.BULK_DELETE_PENDING, priority: 0, visible: conditions.hasPendingBulkMessageDeletion, }, { type: NagbarType.UNCLAIMED_ACCOUNT, priority: 1, visible: conditions.userIsUnclaimed, }, { type: NagbarType.GUILD_MEMBERSHIP_CTA, priority: 2, visible: conditions.canShowGuildMembershipCta, }, { type: NagbarType.EMAIL_VERIFICATION, priority: 3, visible: conditions.userNeedsVerification, }, { type: NagbarType.PREMIUM_GRACE_PERIOD, priority: 4, visible: conditions.canShowPremiumGracePeriod, }, { type: NagbarType.PREMIUM_EXPIRED, priority: 5, visible: conditions.canShowPremiumExpired, }, { type: NagbarType.PREMIUM_ONBOARDING, priority: 6, visible: conditions.canShowPremiumOnboarding, }, { type: NagbarType.DESKTOP_NOTIFICATION, priority: 7, visible: conditions.canShowDesktopNotification, }, { type: NagbarType.GIFT_INVENTORY, priority: 8, visible: conditions.canShowGiftInventory, }, { type: NagbarType.DESKTOP_DOWNLOAD, priority: 9, visible: conditions.canShowDesktopDownload, }, { type: NagbarType.MOBILE_DOWNLOAD, priority: 10, visible: conditions.canShowMobileDownload, }, ]; const visibleNagbars = nagbars.filter((nagbar) => nagbar.visible).sort((a, b) => a.priority - b.priority); const undismissibleNagbars = visibleNagbars.filter((nagbar) => undismissibleTypes.has(nagbar.type)); const regularNagbars = visibleNagbars.filter((nagbar) => !undismissibleTypes.has(nagbar.type)); const selectedRegularNagbars = regularNagbars.length > 0 ? [regularNagbars[0]] : []; return [...undismissibleNagbars, ...selectedRegularNagbars].sort((a, b) => a.priority - b.priority); }, [conditions]); };