feat(nagbar): add invite CTA to Fluxer HQ (#68)

This commit is contained in:
Hampus 2026-01-07 16:06:43 +01:00 committed by GitHub
parent 288477a5b3
commit a61b5802f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 10863 additions and 10579 deletions

View File

@ -28,9 +28,17 @@ interface NagbarButtonProps {
isMobile: boolean;
className?: string;
disabled?: boolean;
submitting?: boolean;
}
export const NagbarButton = ({children, onClick, isMobile, className, disabled = false}: NagbarButtonProps) => {
export const NagbarButton = ({
children,
onClick,
isMobile,
className,
disabled = false,
submitting,
}: NagbarButtonProps) => {
return (
<Button
variant="inverted-outline"
@ -40,6 +48,7 @@ export const NagbarButton = ({children, onClick, isMobile, className, disabled =
className={clsx(styles.button, className)}
onClick={onClick}
disabled={disabled}
submitting={submitting}
>
{children}
</Button>

View File

@ -25,6 +25,7 @@ import {DesktopDownloadNagbar} from './nagbars/DesktopDownloadNagbar';
import {DesktopNotificationNagbar} from './nagbars/DesktopNotificationNagbar';
import {EmailVerificationNagbar} from './nagbars/EmailVerificationNagbar';
import {GiftInventoryNagbar} from './nagbars/GiftInventoryNagbar';
import {GuildMembershipCtaNagbar} from './nagbars/GuildMembershipCtaNagbar';
import {MobileDownloadNagbar} from './nagbars/MobileDownloadNagbar';
import {PendingBulkDeletionNagbar} from './nagbars/PendingBulkDeletionNagbar';
import {PremiumExpiredNagbar} from './nagbars/PremiumExpiredNagbar';
@ -66,6 +67,8 @@ export const NagbarContainer: React.FC<NagbarContainerProps> = observer(({nagbar
return <DesktopDownloadNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
case NagbarType.MOBILE_DOWNLOAD:
return <MobileDownloadNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
case NagbarType.GUILD_MEMBERSHIP_CTA:
return <GuildMembershipCtaNagbar key={nagbar.type} isMobile={mobileLayout.enabled} />;
default:
return null;
}

View File

@ -162,6 +162,13 @@ export const useNagbarConditions = (): NagbarConditions => {
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
@ -185,6 +192,7 @@ export const useNagbarConditions = (): NagbarConditions => {
canShowDesktopDownload,
canShowMobileDownload,
hasPendingBulkMessageDeletion,
canShowGuildMembershipCta,
};
};
@ -208,23 +216,28 @@ export const useActiveNagbars = (conditions: NagbarConditions): Array<NagbarStat
visible: conditions.userIsUnclaimed,
},
{
type: NagbarType.EMAIL_VERIFICATION,
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: 3,
priority: 4,
visible: conditions.canShowPremiumGracePeriod,
},
{
type: NagbarType.PREMIUM_EXPIRED,
priority: 4,
priority: 5,
visible: conditions.canShowPremiumExpired,
},
{
type: NagbarType.PREMIUM_ONBOARDING,
priority: 5,
priority: 6,
visible: conditions.canShowPremiumOnboarding,
},
{

View File

@ -0,0 +1,103 @@
/*
* 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 {Nagbar} from '~/components/layout/Nagbar';
import {NagbarButton} from '~/components/layout/NagbarButton';
import {NagbarContent} from '~/components/layout/NagbarContent';
import {InviteAcceptModal} from '~/components/modals/InviteAcceptModal';
import AuthenticationStore from '~/stores/AuthenticationStore';
import GuildMemberStore from '~/stores/GuildMemberStore';
import InviteStore from '~/stores/InviteStore';
import NagbarStore from '~/stores/NagbarStore';
import {isGuildInvite} from '~/types/InviteTypes';
const FLUXER_HQ_INVITE_CODE = 'fluxer-hq';
export const GuildMembershipCtaNagbar = observer(({isMobile}: {isMobile: boolean}) => {
const currentUserId = AuthenticationStore.currentUserId;
const inviteState = InviteStore.invites.get(FLUXER_HQ_INVITE_CODE);
const invite = inviteState?.data ?? null;
const [isSubmitting, setIsSubmitting] = React.useState(false);
if (!currentUserId) {
return null;
}
if (invite && isGuildInvite(invite)) {
const guildId = invite.guild.id;
const isMember = Boolean(GuildMemberStore.getMember(guildId, currentUserId));
if (isMember) {
return null;
}
}
const handleJoinGuild = async () => {
if (isSubmitting) return;
setIsSubmitting(true);
try {
const module = await import('~/actions/InviteActionCreators');
await module.fetchWithCoalescing(FLUXER_HQ_INVITE_CODE);
const loadedInvite = InviteStore.invites.get(FLUXER_HQ_INVITE_CODE)?.data ?? null;
if (loadedInvite && isGuildInvite(loadedInvite)) {
const guildId = loadedInvite.guild.id;
const isMember = Boolean(GuildMemberStore.getMember(guildId, currentUserId));
if (isMember) {
NagbarStore.guildMembershipCtaDismissed = true;
return;
}
}
} catch {
} finally {
setIsSubmitting(false);
ModalActionCreators.push(modal(() => <InviteAcceptModal code={FLUXER_HQ_INVITE_CODE} />));
}
};
const handleDismiss = () => {
NagbarStore.guildMembershipCtaDismissed = true;
};
return (
<Nagbar
isMobile={isMobile}
backgroundColor="var(--brand-primary)"
textColor="var(--text-on-brand-primary)"
onDismiss={handleDismiss}
dismissible={true}
>
<NagbarContent
isMobile={isMobile}
message={<Trans>Join Fluxer HQ to chat with the team and stay up to date on the latest!</Trans>}
actions={
<NagbarButton isMobile={isMobile} onClick={handleJoinGuild} submitting={isSubmitting} disabled={isSubmitting}>
<Trans>Join Fluxer HQ</Trans>
</NagbarButton>
}
/>
</Nagbar>
);
});

View File

@ -28,6 +28,7 @@ export const NagbarType = {
BULK_DELETE_PENDING: 'bulk-delete-pending',
DESKTOP_DOWNLOAD: 'desktop-download',
MOBILE_DOWNLOAD: 'mobile-download',
GUILD_MEMBERSHIP_CTA: 'guild-membership-cta',
} as const;
export type NagbarType = (typeof NagbarType)[keyof typeof NagbarType];
@ -53,6 +54,7 @@ export interface NagbarConditions {
canShowDesktopDownload: boolean;
canShowMobileDownload: boolean;
hasPendingBulkMessageDeletion: boolean;
canShowGuildMembershipCta: boolean;
}
export const UPDATE_DISMISS_KEY = 'fluxer_update_dismissed_until';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ export interface NagbarSettings {
mobileDownloadDismissed: boolean;
pendingBulkDeletionDismissed: Record<string, boolean>;
invitesDisabledDismissed: Record<string, boolean>;
guildMembershipCtaDismissed: boolean;
claimAccountModalShownThisSession: boolean;
forceOffline: boolean;
forceEmailVerification: boolean;
@ -49,6 +50,7 @@ export interface NagbarSettings {
forceUpdateAvailable: boolean;
forceDesktopDownload: boolean;
forceMobileDownload: boolean;
forceGuildMembershipCta: boolean;
forceHideOffline: boolean;
forceHideEmailVerification: boolean;
forceHideIOSInstall: boolean;
@ -64,6 +66,7 @@ export interface NagbarSettings {
forceHideUpdateAvailable: boolean;
forceHideDesktopDownload: boolean;
forceHideMobileDownload: boolean;
forceHideGuildMembershipCta: boolean;
}
export type NagbarToggleKey = Exclude<
@ -84,6 +87,7 @@ export class NagbarStore implements NagbarSettings {
mobileDownloadDismissed = false;
pendingBulkDeletionDismissed: Record<string, boolean> = {};
invitesDisabledDismissed: Record<string, boolean> = {};
guildMembershipCtaDismissed = false;
claimAccountModalShownThisSession = false;
forceOffline = false;
forceEmailVerification = false;
@ -100,6 +104,7 @@ export class NagbarStore implements NagbarSettings {
forceUpdateAvailable = false;
forceDesktopDownload = false;
forceMobileDownload = false;
forceGuildMembershipCta = false;
forceHideOffline = false;
forceHideEmailVerification = false;
@ -116,6 +121,7 @@ export class NagbarStore implements NagbarSettings {
forceHideUpdateAvailable = false;
forceHideDesktopDownload = false;
forceHideMobileDownload = false;
forceHideGuildMembershipCta = false;
constructor() {
makeAutoObservable(this, {}, {autoBind: true});
@ -136,6 +142,7 @@ export class NagbarStore implements NagbarSettings {
'mobileDownloadDismissed',
'pendingBulkDeletionDismissed',
'invitesDisabledDismissed',
'guildMembershipCtaDismissed',
]);
}
@ -235,6 +242,14 @@ export class NagbarStore implements NagbarSettings {
return this.forceHideUpdateAvailable;
}
getForceGuildMembershipCta(): boolean {
return this.forceGuildMembershipCta;
}
getForceHideGuildMembershipCta(): boolean {
return this.forceHideGuildMembershipCta;
}
hasPendingBulkDeletionDismissed(scheduleKey: string | null): boolean {
if (!scheduleKey) {
return false;
@ -300,6 +315,7 @@ export class NagbarStore implements NagbarSettings {
this.mobileDownloadDismissed = false;
this.pendingBulkDeletionDismissed = {};
this.invitesDisabledDismissed = {};
this.guildMembershipCtaDismissed = false;
this.claimAccountModalShownThisSession = false;
this.forceOffline = false;
@ -317,6 +333,7 @@ export class NagbarStore implements NagbarSettings {
this.forceUpdateAvailable = false;
this.forceDesktopDownload = false;
this.forceMobileDownload = false;
this.forceGuildMembershipCta = false;
this.forceHideOffline = false;
this.forceHideEmailVerification = false;
@ -333,6 +350,7 @@ export class NagbarStore implements NagbarSettings {
this.forceHideUpdateAvailable = false;
this.forceHideDesktopDownload = false;
this.forceHideMobileDownload = false;
this.forceHideGuildMembershipCta = false;
}
handleGuildUpdate(action: {