initial commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import styles from './NagbarContainer.module.css';
|
||||
import {DesktopDownloadNagbar} from './nagbars/DesktopDownloadNagbar';
|
||||
import {DesktopNotificationNagbar} from './nagbars/DesktopNotificationNagbar';
|
||||
import {EmailVerificationNagbar} from './nagbars/EmailVerificationNagbar';
|
||||
import {GiftInventoryNagbar} from './nagbars/GiftInventoryNagbar';
|
||||
import {MobileDownloadNagbar} from './nagbars/MobileDownloadNagbar';
|
||||
import {PendingBulkDeletionNagbar} from './nagbars/PendingBulkDeletionNagbar';
|
||||
import {PremiumExpiredNagbar} from './nagbars/PremiumExpiredNagbar';
|
||||
import {PremiumGracePeriodNagbar} from './nagbars/PremiumGracePeriodNagbar';
|
||||
import {PremiumOnboardingNagbar} from './nagbars/PremiumOnboardingNagbar';
|
||||
import {UnclaimedAccountNagbar} from './nagbars/UnclaimedAccountNagbar';
|
||||
import {type NagbarState, NagbarType} from './types';
|
||||
|
||||
interface NagbarContainerProps {
|
||||
nagbars: Array<NagbarState>;
|
||||
}
|
||||
|
||||
export const NagbarContainer: React.FC<NagbarContainerProps> = observer(({nagbars}) => {
|
||||
const mobileLayout = MobileLayoutStore;
|
||||
|
||||
if (nagbars.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{nagbars.map((nagbar) => {
|
||||
switch (nagbar.type) {
|
||||
case NagbarType.UNCLAIMED_ACCOUNT:
|
||||
return <UnclaimedAccountNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.EMAIL_VERIFICATION:
|
||||
return <EmailVerificationNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.BULK_DELETE_PENDING:
|
||||
return <PendingBulkDeletionNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.DESKTOP_NOTIFICATION:
|
||||
return <DesktopNotificationNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.PREMIUM_GRACE_PERIOD:
|
||||
return <PremiumGracePeriodNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.PREMIUM_EXPIRED:
|
||||
return <PremiumExpiredNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.PREMIUM_ONBOARDING:
|
||||
return <PremiumOnboardingNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.GIFT_INVENTORY:
|
||||
return <GiftInventoryNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.DESKTOP_DOWNLOAD:
|
||||
return <DesktopDownloadNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
case NagbarType.MOBILE_DOWNLOAD:
|
||||
return <MobileDownloadNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const TopNagbarContext = React.createContext<boolean>(false);
|
||||
261
fluxer_app/src/components/layout/app-layout/hooks.ts
Normal file
261
fluxer_app/src/components/layout/app-layout/hooks.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<string | null>(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),
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarState> => {
|
||||
return React.useMemo(() => {
|
||||
const undismissibleTypes = new Set<NagbarType>([
|
||||
NagbarType.UNCLAIMED_ACCOUNT,
|
||||
NagbarType.EMAIL_VERIFICATION,
|
||||
NagbarType.BULK_DELETE_PENDING,
|
||||
]);
|
||||
|
||||
const nagbars: Array<NagbarState> = [
|
||||
{
|
||||
type: NagbarType.BULK_DELETE_PENDING,
|
||||
priority: 0,
|
||||
visible: conditions.hasPendingBulkMessageDeletion,
|
||||
},
|
||||
{
|
||||
type: NagbarType.UNCLAIMED_ACCOUNT,
|
||||
priority: 1,
|
||||
visible: conditions.userIsUnclaimed,
|
||||
},
|
||||
{
|
||||
type: NagbarType.EMAIL_VERIFICATION,
|
||||
priority: 2,
|
||||
visible: conditions.userNeedsVerification,
|
||||
},
|
||||
{
|
||||
type: NagbarType.PREMIUM_GRACE_PERIOD,
|
||||
priority: 3,
|
||||
visible: conditions.canShowPremiumGracePeriod,
|
||||
},
|
||||
{
|
||||
type: NagbarType.PREMIUM_EXPIRED,
|
||||
priority: 4,
|
||||
visible: conditions.canShowPremiumExpired,
|
||||
},
|
||||
{
|
||||
type: NagbarType.PREMIUM_ONBOARDING,
|
||||
priority: 5,
|
||||
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]);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.platformIcons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
.platformIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {AndroidLogoIcon, AppleLogoIcon, WindowsLogoIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {openExternalUrl} from '~/utils/NativeUtils';
|
||||
import styles from './DesktopDownloadNagbar.module.css';
|
||||
|
||||
export const DesktopDownloadNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const handleDownload = () => {
|
||||
openExternalUrl('https://fluxer.app/download');
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
NagbarActionCreators.dismissNagbar('desktopDownloadDismissed');
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar
|
||||
isMobile={isMobile}
|
||||
backgroundColor="var(--brand-primary)"
|
||||
textColor="var(--text-on-brand-primary)"
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={<Trans>Get the Fluxer desktop app for system-wide push-to-talk and a few other goodies.</Trans>}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<span className={styles.platformIcons}>
|
||||
<AppleLogoIcon weight="fill" className={styles.platformIcon} />
|
||||
<AndroidLogoIcon weight="fill" className={styles.platformIcon} />
|
||||
<WindowsLogoIcon weight="fill" className={styles.platformIcon} />
|
||||
</span>
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDownload}>
|
||||
<Trans>Download</Trans>
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.description {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {ConfirmModal} from '~/components/modals/ConfirmModal';
|
||||
import {usePushSubscriptions} from '~/hooks/usePushSubscriptions';
|
||||
import * as PushSubscriptionService from '~/services/push/PushSubscriptionService';
|
||||
import * as NotificationUtils from '~/utils/NotificationUtils';
|
||||
import {isPwaOnMobileOrTablet} from '~/utils/PwaUtils';
|
||||
import styles from './DesktopNotificationNagbar.module.css';
|
||||
|
||||
export const DesktopNotificationNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const {i18n, t} = useLingui();
|
||||
const isPwaMobile = isPwaOnMobileOrTablet();
|
||||
const {refresh} = usePushSubscriptions(isPwaMobile);
|
||||
|
||||
const handleEnable = () => {
|
||||
if (isPwaMobile) {
|
||||
void (async () => {
|
||||
await PushSubscriptionService.registerPushSubscription();
|
||||
await refresh();
|
||||
})();
|
||||
} else if (typeof Notification === 'undefined') {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={t`Notifications Not Supported`}
|
||||
description={
|
||||
<p>
|
||||
<Trans>Your browser does not support desktop notifications.</Trans>
|
||||
</p>
|
||||
}
|
||||
primaryText={t`OK`}
|
||||
primaryVariant="primary"
|
||||
secondaryText={false}
|
||||
onPrimary={() => {
|
||||
NagbarActionCreators.dismissNagbar('desktopNotificationDismissed');
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
NotificationUtils.requestPermission(i18n);
|
||||
}
|
||||
|
||||
NagbarActionCreators.dismissNagbar('desktopNotificationDismissed');
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
ModalActionCreators.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={t`Disable Desktop Notifications?`}
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<Trans>Enable notifications to stay updated on mentions when you're away from the app.</Trans>
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
<Trans>
|
||||
If you dismiss this, you can always enable desktop notifications later under User Settings >
|
||||
Notifications.
|
||||
</Trans>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
primaryText={t`Enable Notifications`}
|
||||
primaryVariant="primary"
|
||||
secondaryText={t`Dismiss Anyway`}
|
||||
onPrimary={() => {
|
||||
handleEnable();
|
||||
}}
|
||||
onSecondary={() => {
|
||||
NagbarActionCreators.dismissNagbar('desktopNotificationDismissed');
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar
|
||||
isMobile={isMobile}
|
||||
backgroundColor="var(--brand-primary)"
|
||||
textColor="var(--text-on-brand-primary)"
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
isPwaMobile ? (
|
||||
<Trans>
|
||||
Enable push notifications for this installed PWA to keep receiving messages when the browser is
|
||||
backgrounded.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Enable desktop notifications to stay updated on new messages.</Trans>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<NagbarButton isMobile={isMobile} onClick={handleEnable}>
|
||||
<Trans>Enable Notifications</Trans>
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export const EmailVerificationNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const user = UserStore.currentUser;
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const openUserSettings = () => {
|
||||
ModalActionCreators.push(modal(() => <UserSettingsModal initialTab="account_security" />));
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar isMobile={isMobile} backgroundColor="#ea580c" textColor="#ffffff">
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={<Trans>Hey {user.displayName}, please verify your email address.</Trans>}
|
||||
actions={
|
||||
<NagbarButton isMobile={isMobile} onClick={openUserSettings}>
|
||||
<Trans>Open Settings</Trans>
|
||||
</NagbarButton>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Plural, Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export const GiftInventoryNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const currentUser = UserStore.currentUser;
|
||||
const unreadCount = currentUser?.unreadGiftInventoryCount ?? 1;
|
||||
|
||||
const handleOpenGiftInventory = () => {
|
||||
ModalActionCreators.push(modal(() => <UserSettingsModal initialTab="gift_inventory" />));
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar isMobile={isMobile} backgroundColor="var(--brand-primary)" textColor="var(--text-on-brand-primary)">
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
<Plural
|
||||
value={unreadCount}
|
||||
one="You have a new gift code waiting in your Gift Inventory."
|
||||
other="You have # new gift codes waiting in your Gift Inventory."
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<NagbarButton isMobile={isMobile} onClick={handleOpenGiftInventory}>
|
||||
<Trans>View Gift Inventory</Trans>
|
||||
</NagbarButton>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.platformIcons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
.platformIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {AndroidLogoIcon, AppleLogoIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {openExternalUrl} from '~/utils/NativeUtils';
|
||||
import styles from './MobileDownloadNagbar.module.css';
|
||||
|
||||
export const MobileDownloadNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const handleDownload = () => {
|
||||
openExternalUrl('https://fluxer.app/download#mobile');
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
NagbarActionCreators.dismissNagbar('mobileDownloadDismissed');
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar
|
||||
isMobile={isMobile}
|
||||
backgroundColor="var(--brand-primary)"
|
||||
textColor="var(--text-on-brand-primary)"
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
<Trans>
|
||||
Get Fluxer on mobile to receive notifications on the go and stay connected with your friends anytime.
|
||||
</Trans>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<span className={styles.platformIcons}>
|
||||
<AppleLogoIcon weight="fill" className={styles.platformIcon} />
|
||||
<AndroidLogoIcon weight="fill" className={styles.platformIcon} />
|
||||
</span>
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDownload}>
|
||||
<Trans>Download</Trans>
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export const PendingBulkDeletionNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const pending = UserStore.currentUser?.getPendingBulkMessageDeletion();
|
||||
const countFormatter = React.useMemo(() => new Intl.NumberFormat(), []);
|
||||
const scheduleKey = pending?.scheduledAt.toISOString();
|
||||
const handleHideNagbar = React.useCallback(() => {
|
||||
if (!scheduleKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
NagbarActionCreators.dismissPendingBulkDeletionNagbar(scheduleKey);
|
||||
}, [scheduleKey]);
|
||||
|
||||
if (!pending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channelCountLabel = countFormatter.format(pending.channelCount);
|
||||
const messageCountLabel = countFormatter.format(pending.messageCount);
|
||||
const scheduledLabel = pending.scheduledAt.toLocaleString();
|
||||
|
||||
const openDeletionSettings = () => {
|
||||
ModalActionCreators.push(
|
||||
modal(() => <UserSettingsModal initialTab="privacy_safety" initialSubtab="data-deletion" />),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar
|
||||
isMobile={isMobile}
|
||||
backgroundColor="var(--status-danger)"
|
||||
textColor="#ffffff"
|
||||
dismissible
|
||||
onDismiss={handleHideNagbar}
|
||||
>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
<Trans>
|
||||
Deletion of <strong>{messageCountLabel}</strong> messages from <strong>{channelCountLabel}</strong> channels
|
||||
is scheduled for <strong>{scheduledLabel}</strong>. Cancel it from the Privacy Dashboard.
|
||||
</Trans>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleHideNagbar}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<NagbarButton isMobile={isMobile} onClick={openDeletionSettings}>
|
||||
<Trans>Review deletion</Trans>
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import * as PremiumActionCreators from '~/actions/PremiumActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {openExternalUrl} from '~/utils/NativeUtils';
|
||||
|
||||
export const PremiumExpiredNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const user = UserStore.currentUser;
|
||||
const [loadingPortal, setLoadingPortal] = React.useState(false);
|
||||
|
||||
const handleOpenCustomerPortal = async () => {
|
||||
setLoadingPortal(true);
|
||||
try {
|
||||
const url = await PremiumActionCreators.createCustomerPortalSession();
|
||||
void openExternalUrl(url);
|
||||
} catch (error) {
|
||||
console.error('Failed to open customer portal', error);
|
||||
} finally {
|
||||
setLoadingPortal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
NagbarActionCreators.dismissNagbar('premiumExpiredDismissed');
|
||||
};
|
||||
|
||||
if (!user?.premiumUntil || user?.premiumWillCancel) return null;
|
||||
|
||||
return (
|
||||
<Nagbar
|
||||
isMobile={isMobile}
|
||||
backgroundColor="var(--status-danger)"
|
||||
textColor="var(--text-on-brand-primary)"
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
<Trans>
|
||||
Your Plutonium subscription has expired. You've lost all Plutonium perks. Reactivate your subscription to
|
||||
regain access.
|
||||
</Trans>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<NagbarButton isMobile={isMobile} onClick={handleOpenCustomerPortal} disabled={loadingPortal}>
|
||||
{loadingPortal ? <Trans>Opening...</Trans> : <Trans>Reactivate</Trans>}
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import * as PremiumActionCreators from '~/actions/PremiumActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as LocaleUtils from '~/utils/LocaleUtils';
|
||||
import {openExternalUrl} from '~/utils/NativeUtils';
|
||||
|
||||
export const PremiumGracePeriodNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const user = UserStore.currentUser;
|
||||
const [loadingPortal, setLoadingPortal] = React.useState(false);
|
||||
|
||||
const handleOpenCustomerPortal = async () => {
|
||||
setLoadingPortal(true);
|
||||
try {
|
||||
const url = await PremiumActionCreators.createCustomerPortalSession();
|
||||
void openExternalUrl(url);
|
||||
} catch (error) {
|
||||
console.error('Failed to open customer portal', error);
|
||||
} finally {
|
||||
setLoadingPortal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
NagbarActionCreators.dismissNagbar('premiumGracePeriodDismissed');
|
||||
};
|
||||
|
||||
if (!user?.premiumUntil || user?.premiumWillCancel) return null;
|
||||
|
||||
const expiryDate = new Date(user.premiumUntil);
|
||||
const gracePeriodMs = 3 * 24 * 60 * 60 * 1000;
|
||||
const graceEndDate = new Date(expiryDate.getTime() + gracePeriodMs);
|
||||
const locale = LocaleUtils.getCurrentLocale();
|
||||
|
||||
const formattedGraceDate = graceEndDate.toLocaleDateString(locale, {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
return (
|
||||
<Nagbar isMobile={isMobile} backgroundColor="#f97316" textColor="#ffffff" dismissible onDismiss={handleDismiss}>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
<Trans>
|
||||
Your subscription failed to renew, but you still have access to Plutonium perks until{' '}
|
||||
<strong>{formattedGraceDate}</strong>. Take action now or you'll lose all perks.
|
||||
</Trans>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<NagbarButton isMobile={isMobile} onClick={handleOpenCustomerPortal} disabled={loadingPortal}>
|
||||
{loadingPortal ? <Trans>Opening...</Trans> : <Trans>Manage Subscription</Trans>}
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as UserActionCreators from '~/actions/UserActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
|
||||
export const PremiumOnboardingNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const handleOpenPremiumSettings = () => {
|
||||
void UserActionCreators.update({has_dismissed_premium_onboarding: true});
|
||||
ModalActionCreators.push(modal(() => <UserSettingsModal initialTab="plutonium" />));
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
void UserActionCreators.update({has_dismissed_premium_onboarding: true});
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar
|
||||
isMobile={isMobile}
|
||||
backgroundColor="var(--brand-primary)"
|
||||
textColor="var(--text-on-brand-primary)"
|
||||
dismissible
|
||||
onDismiss={handleDismiss}
|
||||
>
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={
|
||||
<Trans>Welcome to Fluxer Plutonium! Explore your premium features and manage your subscription.</Trans>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{isMobile && (
|
||||
<NagbarButton isMobile={isMobile} onClick={handleDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
</NagbarButton>
|
||||
)}
|
||||
<NagbarButton isMobile={isMobile} onClick={handleOpenPremiumSettings}>
|
||||
<Trans>View Premium Features</Trans>
|
||||
</NagbarButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
import {NagbarButton} from '~/components/layout/NagbarButton';
|
||||
import {NagbarContent} from '~/components/layout/NagbarContent';
|
||||
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export const UnclaimedAccountNagbar = observer(({isMobile}: {isMobile: boolean}) => {
|
||||
const user = UserStore.currentUser;
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClaimAccount = () => {
|
||||
ModalActionCreators.push(modal(() => <ClaimAccountModal />));
|
||||
};
|
||||
|
||||
return (
|
||||
<Nagbar isMobile={isMobile} backgroundColor="#ea580c" textColor="#ffffff">
|
||||
<NagbarContent
|
||||
isMobile={isMobile}
|
||||
message={<Trans>Hey {user.displayName}, claim your account to prevent losing access.</Trans>}
|
||||
actions={
|
||||
<NagbarButton isMobile={isMobile} onClick={handleClaimAccount}>
|
||||
<Trans>Claim Account</Trans>
|
||||
</NagbarButton>
|
||||
}
|
||||
/>
|
||||
</Nagbar>
|
||||
);
|
||||
});
|
||||
58
fluxer_app/src/components/layout/app-layout/types.ts
Normal file
58
fluxer_app/src/components/layout/app-layout/types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const NagbarType = {
|
||||
UNCLAIMED_ACCOUNT: 'unclaimed-account',
|
||||
EMAIL_VERIFICATION: 'email-verification',
|
||||
DESKTOP_NOTIFICATION: 'desktop-notification',
|
||||
PREMIUM_GRACE_PERIOD: 'premium-grace-period',
|
||||
PREMIUM_EXPIRED: 'premium-expired',
|
||||
PREMIUM_ONBOARDING: 'premium-onboarding',
|
||||
GIFT_INVENTORY: 'gift-inventory',
|
||||
BULK_DELETE_PENDING: 'bulk-delete-pending',
|
||||
DESKTOP_DOWNLOAD: 'desktop-download',
|
||||
MOBILE_DOWNLOAD: 'mobile-download',
|
||||
} as const;
|
||||
|
||||
export type NagbarType = (typeof NagbarType)[keyof typeof NagbarType];
|
||||
|
||||
export interface NagbarState {
|
||||
type: NagbarType;
|
||||
priority: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface AppLayoutState {
|
||||
isStandalone: boolean;
|
||||
}
|
||||
|
||||
export interface NagbarConditions {
|
||||
userIsUnclaimed: boolean;
|
||||
userNeedsVerification: boolean;
|
||||
canShowDesktopNotification: boolean;
|
||||
canShowPremiumGracePeriod: boolean;
|
||||
canShowPremiumExpired: boolean;
|
||||
canShowPremiumOnboarding: boolean;
|
||||
canShowGiftInventory: boolean;
|
||||
canShowDesktopDownload: boolean;
|
||||
canShowMobileDownload: boolean;
|
||||
hasPendingBulkMessageDeletion: boolean;
|
||||
}
|
||||
|
||||
export const UPDATE_DISMISS_KEY = 'fluxer_update_dismissed_until';
|
||||
Reference in New Issue
Block a user