initial commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
.sliderContainer {
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sliderLabel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.labelText {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.labelDescription {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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, useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
|
||||
import * as UserActionCreators from '~/actions/UserActionCreators';
|
||||
import {UserPremiumTypes} from '~/Constants';
|
||||
import {Select} from '~/components/form/Select';
|
||||
import {Switch} from '~/components/form/Switch';
|
||||
import {SettingsTabSection} from '~/components/modals/shared/SettingsTabLayout';
|
||||
import {Slider} from '~/components/uikit/Slider';
|
||||
import type {UserRecord} from '~/records/UserRecord';
|
||||
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
|
||||
import styles from './AccountPremiumTab.module.css';
|
||||
import {
|
||||
applyPremiumScenarioOption,
|
||||
PREMIUM_SCENARIO_OPTIONS,
|
||||
type PremiumScenarioOption,
|
||||
} from './premiumScenarioOptions';
|
||||
|
||||
interface AccountPremiumTabContentProps {
|
||||
user: UserRecord;
|
||||
}
|
||||
|
||||
export const AccountPremiumTabContent: React.FC<AccountPremiumTabContentProps> = observer(({user}) => {
|
||||
const {t} = useLingui();
|
||||
const premiumTypeOptions = [
|
||||
{value: '', label: t`Use Actual Premium Type`},
|
||||
{value: UserPremiumTypes.NONE.toString(), label: t`None`},
|
||||
{value: UserPremiumTypes.SUBSCRIPTION.toString(), label: t`Subscription`},
|
||||
{value: UserPremiumTypes.LIFETIME.toString(), label: t`Lifetime`},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTabSection title={<Trans>Account State Overrides</Trans>}>
|
||||
<Switch
|
||||
label={t`Email Verified Override`}
|
||||
value={DeveloperOptionsStore.emailVerifiedOverride ?? false}
|
||||
description={t`Override email verification status`}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption('emailVerifiedOverride', value ? true : null)
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Unclaimed Account Override`}
|
||||
value={DeveloperOptionsStore.unclaimedAccountOverride ?? false}
|
||||
description={t`Override unclaimed account status`}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption('unclaimedAccountOverride', value ? true : null)
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Unread Gift Inventory Override`}
|
||||
value={DeveloperOptionsStore.hasUnreadGiftInventoryOverride ?? false}
|
||||
description={t`Override unread gift inventory status`}
|
||||
onChange={(value) => {
|
||||
DeveloperOptionsActionCreators.updateOption('hasUnreadGiftInventoryOverride', value ? true : null);
|
||||
if (!value) DeveloperOptionsActionCreators.updateOption('unreadGiftInventoryCountOverride', null);
|
||||
}}
|
||||
/>
|
||||
{DeveloperOptionsStore.hasUnreadGiftInventoryOverride && (
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderLabel}>
|
||||
<span className={styles.labelText}>
|
||||
<Trans>Unread Gift Count</Trans>
|
||||
</span>
|
||||
<p className={styles.labelDescription}>
|
||||
<Trans>Set the number of unread gifts in inventory.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={DeveloperOptionsStore.unreadGiftInventoryCountOverride ?? 1}
|
||||
factoryDefaultValue={1}
|
||||
minValue={0}
|
||||
maxValue={99}
|
||||
step={1}
|
||||
markers={[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 99]}
|
||||
stickToMarkers={false}
|
||||
onMarkerRender={(v) => `${v}`}
|
||||
onValueRender={(v) => <Plural value={v} one="# gift" other="# gifts" />}
|
||||
onValueChange={(v) => DeveloperOptionsActionCreators.updateOption('unreadGiftInventoryCountOverride', v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Premium Type Override</Trans>}>
|
||||
<Select
|
||||
label={t`Override Premium Type`}
|
||||
value={DeveloperOptionsStore.premiumTypeOverride?.toString() ?? ''}
|
||||
options={premiumTypeOptions}
|
||||
onChange={(value) => {
|
||||
const premiumTypeOverride = value === '' ? null : Number.parseInt(value, 10);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', premiumTypeOverride);
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Backend Premium Override`}
|
||||
value={user.premiumEnabledOverride ?? false}
|
||||
description={t`Toggle premium_enabled_override on the backend`}
|
||||
onChange={async (value) => {
|
||||
await UserActionCreators.update({premium_enabled_override: value});
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Has Ever Purchased Override`}
|
||||
value={DeveloperOptionsStore.hasEverPurchasedOverride ?? false}
|
||||
description={t`Simulates having a Stripe customer ID (for testing purchase history access)`}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', value ? true : null)
|
||||
}
|
||||
/>
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Premium Subscription Scenarios</Trans>}>
|
||||
<Select<PremiumScenarioOption>
|
||||
label={t`Test Subscription State`}
|
||||
value="none"
|
||||
options={PREMIUM_SCENARIO_OPTIONS.map(({value, label}) => ({
|
||||
value,
|
||||
label: t(label),
|
||||
}))}
|
||||
onChange={applyPremiumScenarioOption}
|
||||
/>
|
||||
</SettingsTabSection>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
.toggleGroup {
|
||||
border-top: 1px solid var(--background-header-secondary);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.toggleGroupFirst {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.groupTitle {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-tertiary-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.toggleList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
|
||||
import {Switch} from '~/components/form/Switch';
|
||||
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
|
||||
import styles from './GeneralTab.module.css';
|
||||
import {getToggleGroups} from './shared';
|
||||
|
||||
export const GeneralTabContent: React.FC = observer(() => {
|
||||
const {t} = useLingui();
|
||||
const toggleGroups = getToggleGroups();
|
||||
|
||||
return (
|
||||
<>
|
||||
{toggleGroups.map((group, gi) => (
|
||||
<div
|
||||
key={group.title.id ?? `toggle-group-${gi}`}
|
||||
className={gi > 0 ? styles.toggleGroup : styles.toggleGroupFirst}
|
||||
>
|
||||
<div className={styles.groupTitle}>{t(group.title)}</div>
|
||||
<div className={styles.toggleList}>
|
||||
{group.items.map(({key, label, description}) => (
|
||||
<Switch
|
||||
key={String(key)}
|
||||
label={t(label)}
|
||||
description={description ? t(description) : undefined}
|
||||
value={Boolean(DeveloperOptionsStore[key])}
|
||||
onChange={(value) => {
|
||||
DeveloperOptionsActionCreators.updateOption(key, value);
|
||||
if (key === 'selfHostedModeOverride') {
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {SettingsSection} from '~/components/modals/shared/SettingsSection';
|
||||
import ConnectionStore from '~/stores/ConnectionStore';
|
||||
import NagbarStore from '~/stores/NagbarStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {AccountPremiumTabContent} from './AccountPremiumTab';
|
||||
import {GeneralTabContent} from './GeneralTab';
|
||||
import styles from './Inline.module.css';
|
||||
import {MockingTabContent} from './MockingTab';
|
||||
import {NagbarsTabContent} from './NagbarsTab';
|
||||
import {ToolsTabContent} from './ToolsTab';
|
||||
|
||||
export const DeveloperOptionsInlineTab: React.FC = observer(() => {
|
||||
const {t} = useLingui();
|
||||
const socket = ConnectionStore.socket;
|
||||
const nagbarState = NagbarStore;
|
||||
const user = UserStore.currentUser;
|
||||
|
||||
if (!(user && socket)) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SettingsSection id="dev-general" title={t`General`}>
|
||||
<GeneralTabContent />
|
||||
</SettingsSection>
|
||||
<SettingsSection id="dev-account-premium" title={t`Account & Premium`}>
|
||||
<AccountPremiumTabContent user={user} />
|
||||
</SettingsSection>
|
||||
<SettingsSection id="dev-mocking" title={t`Mocking`}>
|
||||
<MockingTabContent />
|
||||
</SettingsSection>
|
||||
<SettingsSection id="dev-nagbars" title={t`Nagbars`}>
|
||||
<NagbarsTabContent nagbarState={nagbarState} />
|
||||
</SettingsSection>
|
||||
<SettingsSection id="dev-tools" title={t`Tools`}>
|
||||
<ToolsTabContent socket={socket} />
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sliderLabel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.labelText {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.labelDescription {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: var(--text-primary-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* 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 type React from 'react';
|
||||
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {Select} from '~/components/form/Select';
|
||||
import {Switch} from '~/components/form/Switch';
|
||||
import RequiredActionModal from '~/components/modals/RequiredActionModal';
|
||||
import {SettingsTabSection} from '~/components/modals/shared/SettingsTabLayout';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {Slider} from '~/components/uikit/Slider';
|
||||
import type {DeveloperOptionsState} from '~/stores/DeveloperOptionsStore';
|
||||
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import styles from './MockingTab.module.css';
|
||||
|
||||
export const MockingTabContent: React.FC = observer(() => {
|
||||
const {t} = useLingui();
|
||||
const handleClearAttachmentMocks = () => {
|
||||
DeveloperOptionsActionCreators.clearAllAttachmentMocks();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsTabSection title={<Trans>Verification Barriers</Trans>}>
|
||||
<Select
|
||||
label={t`Mock Verification Barrier`}
|
||||
value={DeveloperOptionsStore.mockVerificationBarrier}
|
||||
options={[
|
||||
{value: 'none', label: t`None (Normal Behavior)`},
|
||||
{value: 'unclaimed_account', label: t`Unclaimed Account`},
|
||||
{value: 'unverified_email', label: t`Unverified Email`},
|
||||
{value: 'account_too_new', label: t`Account Too New`},
|
||||
{value: 'not_member_long', label: t`Not Member Long Enough`},
|
||||
{value: 'no_phone', label: t`No Phone Number`},
|
||||
{value: 'send_message_disabled', label: t`Send Message Disabled`},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockVerificationBarrier',
|
||||
value as DeveloperOptionsState['mockVerificationBarrier'],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{(DeveloperOptionsStore.mockVerificationBarrier === 'account_too_new' ||
|
||||
DeveloperOptionsStore.mockVerificationBarrier === 'not_member_long') && (
|
||||
<Select<string>
|
||||
label={t`Countdown Timer`}
|
||||
value={DeveloperOptionsStore.mockBarrierTimeRemaining?.toString() ?? '300000'}
|
||||
options={[
|
||||
{value: '0', label: t`No Timer`},
|
||||
{value: '10000', label: t`10 seconds`},
|
||||
{value: '30000', label: t`30 seconds`},
|
||||
{value: '60000', label: t`1 minute`},
|
||||
{value: '120000', label: t`2 minutes`},
|
||||
{value: '300000', label: t`5 minutes`},
|
||||
{value: '600000', label: t`10 minutes`},
|
||||
]}
|
||||
onChange={(v) =>
|
||||
DeveloperOptionsActionCreators.updateOption('mockBarrierTimeRemaining', Number.parseInt(v, 10))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Channel Permissions</Trans>}>
|
||||
<Switch
|
||||
label={t`Force No SEND_MESSAGES Permission`}
|
||||
value={DeveloperOptionsStore.forceNoSendMessages}
|
||||
description={t`Removes SEND_MESSAGES permission in all channels (disables textarea and all buttons)`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('forceNoSendMessages', value)}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Force No ATTACH_FILES Permission`}
|
||||
value={DeveloperOptionsStore.forceNoAttachFiles}
|
||||
description={t`Removes ATTACH_FILES permission in all channels (hides upload button)`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('forceNoAttachFiles', value)}
|
||||
/>
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Attachments</Trans>}>
|
||||
<Button variant="secondary" onClick={handleClearAttachmentMocks}>
|
||||
<Trans>Clear attachment mocks</Trans>
|
||||
</Button>
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Voice Calls</Trans>}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => DeveloperOptionsActionCreators.triggerMockIncomingCall()}
|
||||
disabled={!UserStore.currentUser}
|
||||
>
|
||||
<Trans>Trigger Mock Incoming Call</Trans>
|
||||
</Button>
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Slowmode</Trans>}>
|
||||
<Switch
|
||||
label={t`Force Slowmode Active`}
|
||||
value={DeveloperOptionsStore.mockSlowmodeActive}
|
||||
description={t`Forces slowmode to be active in all channels`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockSlowmodeActive', value)}
|
||||
/>
|
||||
{DeveloperOptionsStore.mockSlowmodeActive && (
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderLabel}>
|
||||
<span className={styles.labelText}>
|
||||
<Trans>Slowmode Time Remaining (ms)</Trans>
|
||||
</span>
|
||||
<p className={styles.labelDescription}>
|
||||
<Trans>Set how much time remains before the user can send another message.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={DeveloperOptionsStore.mockSlowmodeRemaining}
|
||||
factoryDefaultValue={10000}
|
||||
minValue={0}
|
||||
maxValue={60000}
|
||||
step={1000}
|
||||
markers={[0, 5000, 10000, 15000, 20000, 30000, 45000, 60000]}
|
||||
stickToMarkers={false}
|
||||
onMarkerRender={(v) => `${v / 1000}s`}
|
||||
onValueRender={(v) => <Trans>{Math.floor(v / 1000)} seconds</Trans>}
|
||||
onValueChange={(v) => DeveloperOptionsActionCreators.updateOption('mockSlowmodeRemaining', v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection
|
||||
title={
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.headerTitle}>
|
||||
<Trans>Visionary</Trans>
|
||||
</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={() => {
|
||||
DeveloperOptionsActionCreators.updateOption('mockVisionarySoldOut', false);
|
||||
DeveloperOptionsActionCreators.updateOption('mockVisionaryRemaining', null);
|
||||
}}
|
||||
>
|
||||
<Trans>Reset to Real Data</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
label={t`Mock Visionary Sold Out`}
|
||||
value={DeveloperOptionsStore.mockVisionarySoldOut}
|
||||
description={t`Force Visionary to appear sold out`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockVisionarySoldOut', value)}
|
||||
/>
|
||||
{!DeveloperOptionsStore.mockVisionarySoldOut && (
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderLabel}>
|
||||
<span className={styles.labelText}>
|
||||
<Trans>Mock Visionary Remaining</Trans>
|
||||
</span>
|
||||
<p className={styles.labelDescription}>
|
||||
<Trans>Set a fixed remaining count for Visionary availability.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Slider
|
||||
defaultValue={DeveloperOptionsStore.mockVisionaryRemaining ?? 100}
|
||||
factoryDefaultValue={100}
|
||||
minValue={0}
|
||||
maxValue={1000}
|
||||
step={10}
|
||||
markers={[0, 50, 100, 250, 500, 750, 1000]}
|
||||
stickToMarkers={false}
|
||||
onMarkerRender={(v) => `${v}`}
|
||||
onValueRender={(v) => <Trans>{v} remaining</Trans>}
|
||||
onValueChange={(v) => DeveloperOptionsActionCreators.updateOption('mockVisionaryRemaining', v)}
|
||||
/>
|
||||
<div className={styles.note}>
|
||||
<Trans>Set to 0 to simulate sold out.</Trans>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Required Actions</Trans>}>
|
||||
<Select
|
||||
label={t`Mock Variant`}
|
||||
value={DeveloperOptionsStore.mockRequiredActionsMode}
|
||||
description={t`Choose which variant to preview`}
|
||||
options={[
|
||||
{value: 'email', label: t`Email`},
|
||||
{value: 'phone', label: t`Phone`},
|
||||
{value: 'email_or_phone', label: t`Email or Phone`},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockRequiredActionsMode',
|
||||
value as DeveloperOptionsState['mockRequiredActionsMode'],
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className={styles.buttonRow}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
ModalActionCreators.pushWithKey(
|
||||
modal(() => <RequiredActionModal mock={true} />),
|
||||
'required-actions-mock',
|
||||
)
|
||||
}
|
||||
disabled={!UserStore.currentUser}
|
||||
>
|
||||
<Trans>Open Overlay</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{DeveloperOptionsStore.mockRequiredActionsMode === 'email_or_phone' && (
|
||||
<Select
|
||||
label={t`Default Tab`}
|
||||
value={DeveloperOptionsStore.mockRequiredActionsSelectedTab}
|
||||
description={t`Which tab is selected when the overlay opens`}
|
||||
options={[
|
||||
{value: 'email', label: t`Email`},
|
||||
{value: 'phone', label: t`Phone`},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockRequiredActionsSelectedTab',
|
||||
value as DeveloperOptionsState['mockRequiredActionsSelectedTab'],
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(DeveloperOptionsStore.mockRequiredActionsMode === 'phone' ||
|
||||
(DeveloperOptionsStore.mockRequiredActionsMode === 'email_or_phone' &&
|
||||
DeveloperOptionsStore.mockRequiredActionsSelectedTab === 'phone')) && (
|
||||
<Select
|
||||
label={t`Phone Step`}
|
||||
value={DeveloperOptionsStore.mockRequiredActionsPhoneStep}
|
||||
description={t`Pick which phone step to show`}
|
||||
options={[
|
||||
{value: 'phone', label: t`Enter Phone`},
|
||||
{value: 'code', label: t`Enter Code`},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockRequiredActionsPhoneStep',
|
||||
value as DeveloperOptionsState['mockRequiredActionsPhoneStep'],
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Switch
|
||||
label={t`Use Reverification Text`}
|
||||
value={DeveloperOptionsStore.mockRequiredActionsReverify}
|
||||
description={t`Swap text to reverify variants`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockRequiredActionsReverify', value)}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={t`Resend Button Loading`}
|
||||
value={DeveloperOptionsStore.mockRequiredActionsResending}
|
||||
description={t`Force the email resend button into a loading state`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockRequiredActionsResending', value)}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label={t`Resend Outcome`}
|
||||
value={DeveloperOptionsStore.mockRequiredActionsResendOutcome}
|
||||
description={t`Toast shown when clicking resend in mock mode`}
|
||||
options={[
|
||||
{value: 'success', label: t`Success`},
|
||||
{value: 'rate_limited', label: t`Rate Limited`},
|
||||
{value: 'server_error', label: t`Server Error`},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockRequiredActionsResendOutcome',
|
||||
value as DeveloperOptionsState['mockRequiredActionsResendOutcome'],
|
||||
)
|
||||
}
|
||||
/>
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>NSFW Gates</Trans>}>
|
||||
<Select
|
||||
label={t`Mock NSFW Channel Gate Reason`}
|
||||
value={DeveloperOptionsStore.mockNSFWGateReason}
|
||||
description={t`Mock gate reason for blocking entire NSFW channels`}
|
||||
options={[
|
||||
{value: 'none', label: t`None (Normal Behavior)`},
|
||||
{value: 'geo_restricted', label: t`Geo Restricted`},
|
||||
{value: 'age_restricted', label: t`Age Restricted`},
|
||||
{value: 'consent_required', label: t`Consent Required`},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockNSFWGateReason',
|
||||
value as DeveloperOptionsState['mockNSFWGateReason'],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
label={t`Mock NSFW Media Gate Reason`}
|
||||
value={DeveloperOptionsStore.mockNSFWMediaGateReason}
|
||||
description={t`Forces all media to NSFW and shows the selected blur overlay error state`}
|
||||
options={[
|
||||
{value: 'none', label: t`None (Normal Behavior)`},
|
||||
{value: 'geo_restricted', label: t`Geo Restricted`},
|
||||
{value: 'age_restricted', label: t`Age Restricted`},
|
||||
]}
|
||||
onChange={(value) => {
|
||||
DeveloperOptionsActionCreators.updateOption(
|
||||
'mockNSFWMediaGateReason',
|
||||
value as DeveloperOptionsState['mockNSFWMediaGateReason'],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Mock Geo Block Overlay`}
|
||||
value={DeveloperOptionsStore.mockGeoBlocked}
|
||||
description={t`Show the geo-blocked overlay (dismissible)`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockGeoBlocked', value)}
|
||||
/>
|
||||
</SettingsTabSection>
|
||||
|
||||
<SettingsTabSection title={<Trans>Gift Inventory</Trans>}>
|
||||
<Switch
|
||||
label={t`Mock Gift Inventory`}
|
||||
value={DeveloperOptionsStore.mockGiftInventory ?? false}
|
||||
description={t`Show a mock gift code in your gift inventory for testing`}
|
||||
onChange={(value) => {
|
||||
DeveloperOptionsActionCreators.updateOption('mockGiftInventory', value ? true : null);
|
||||
if (!value) {
|
||||
DeveloperOptionsActionCreators.updateOption('mockGiftDurationMonths', 12);
|
||||
DeveloperOptionsActionCreators.updateOption('mockGiftRedeemed', null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{DeveloperOptionsStore.mockGiftInventory && (
|
||||
<>
|
||||
<Select<string>
|
||||
label={t`Gift Duration`}
|
||||
value={DeveloperOptionsStore.mockGiftDurationMonths?.toString() ?? '12'}
|
||||
description={t`How many months of Plutonium the mock gift provides`}
|
||||
options={[
|
||||
{value: '1', label: t`1 Month`},
|
||||
{value: '3', label: t`3 Months`},
|
||||
{value: '6', label: t`6 Months`},
|
||||
{value: '12', label: t`12 Months (1 Year)`},
|
||||
{value: '0', label: t`Lifetime`},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
DeveloperOptionsActionCreators.updateOption('mockGiftDurationMonths', Number.parseInt(value, 10))
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
label={t`Mark as Redeemed`}
|
||||
value={DeveloperOptionsStore.mockGiftRedeemed ?? false}
|
||||
description={t`Show the gift as already redeemed`}
|
||||
onChange={(value) => DeveloperOptionsActionCreators.updateOption('mockGiftRedeemed', value ? true : null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsTabSection>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
.nagbarList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nagbarItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nagbarInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nagbarLabel {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.nagbarStatus {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer > * {
|
||||
flex: 1;
|
||||
min-width: fit-content;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import type {NagbarStore, NagbarToggleKey} from '~/stores/NagbarStore';
|
||||
import styles from './NagbarsTab.module.css';
|
||||
import {getNagbarControls, type NagbarControlDefinition} from './nagbarControls';
|
||||
|
||||
interface NagbarsTabContentProps {
|
||||
nagbarState: NagbarStore;
|
||||
}
|
||||
|
||||
const NagbarRow: React.FC<{
|
||||
control: NagbarControlDefinition;
|
||||
nagbarState: NagbarStore;
|
||||
}> = observer(({control, nagbarState}) => {
|
||||
const {t} = useLingui();
|
||||
const getFlag = (key: NagbarToggleKey): boolean => Boolean(nagbarState[key]);
|
||||
const useActualDisabled = control.useActualDisabled?.(nagbarState) ?? control.resetKeys.every((key) => !getFlag(key));
|
||||
const forceShowDisabled = control.forceShowDisabled?.(nagbarState) ?? getFlag(control.forceKey);
|
||||
const forceHideDisabled = control.forceHideDisabled?.(nagbarState) ?? getFlag(control.forceHideKey);
|
||||
|
||||
const handleUseActual = () => {
|
||||
control.resetKeys.forEach((key) => NagbarActionCreators.resetNagbar(key));
|
||||
NagbarActionCreators.setForceHideNagbar(control.forceHideKey, false);
|
||||
};
|
||||
|
||||
const handleForceShow = () => {
|
||||
NagbarActionCreators.dismissNagbar(control.forceKey);
|
||||
NagbarActionCreators.setForceHideNagbar(control.forceHideKey, false);
|
||||
};
|
||||
|
||||
const handleForceHide = () => {
|
||||
NagbarActionCreators.setForceHideNagbar(control.forceHideKey, true);
|
||||
NagbarActionCreators.resetNagbar(control.forceKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.nagbarItem}>
|
||||
<div className={styles.nagbarInfo}>
|
||||
<span className={styles.nagbarLabel}>{control.label}</span>
|
||||
<span className={styles.nagbarStatus}>{control.status(nagbarState)}</span>
|
||||
</div>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button onClick={handleUseActual} disabled={useActualDisabled}>
|
||||
{t`Use Actual`}
|
||||
</Button>
|
||||
<Button onClick={handleForceShow} disabled={forceShowDisabled}>
|
||||
{t`Force Show`}
|
||||
</Button>
|
||||
<Button onClick={handleForceHide} disabled={forceHideDisabled}>
|
||||
{t`Force Hide`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const NagbarsTabContent: React.FC<NagbarsTabContentProps> = observer(({nagbarState}) => {
|
||||
const {t} = useLingui();
|
||||
const nagbarControls = getNagbarControls(t);
|
||||
|
||||
return (
|
||||
<div className={styles.nagbarList}>
|
||||
{nagbarControls.map((control) => (
|
||||
<NagbarRow key={control.key} control={control} nagbarState={nagbarState} />
|
||||
))}
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
onClick={() => NagbarActionCreators.resetAllNagbars()}
|
||||
variant="secondary"
|
||||
>{t`Reset All Nagbars`}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback, useState} from 'react';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {testBulkDeleteAllMessages} from '~/actions/UserActionCreators';
|
||||
import {CaptchaModal} from '~/components/modals/CaptchaModal';
|
||||
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {KeyboardModeIntroModal} from '~/components/modals/KeyboardModeIntroModal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import type {GatewaySocket} from '~/lib/GatewaySocket';
|
||||
import NewDeviceMonitoringStore from '~/stores/NewDeviceMonitoringStore';
|
||||
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
|
||||
import styles from './ToolsTab.module.css';
|
||||
|
||||
interface ToolsTabContentProps {
|
||||
socket: GatewaySocket;
|
||||
}
|
||||
|
||||
export const ToolsTabContent: React.FC<ToolsTabContentProps> = observer(({socket}) => {
|
||||
const {t} = useLingui();
|
||||
const [isTestingBulkDelete, setIsTestingBulkDelete] = useState(false);
|
||||
const [shouldCrash, setShouldCrash] = useState(false);
|
||||
|
||||
const handleTestBulkDelete = useCallback(async () => {
|
||||
setIsTestingBulkDelete(true);
|
||||
try {
|
||||
await testBulkDeleteAllMessages();
|
||||
} finally {
|
||||
setIsTestingBulkDelete(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOpenCaptchaModal = useCallback(() => {
|
||||
ModalActionCreators.push(
|
||||
ModalActionCreators.modal(() => (
|
||||
<CaptchaModal
|
||||
closeOnVerify={false}
|
||||
onVerify={(token, captchaType) => {
|
||||
console.debug('Captcha solved in Developer Options', {token, captchaType});
|
||||
}}
|
||||
onCancel={() => {
|
||||
console.debug('Captcha cancelled in Developer Options');
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleOpenClaimAccountModal = useCallback(() => {
|
||||
ModalActionCreators.push(ModalActionCreators.modal(() => <ClaimAccountModal />));
|
||||
}, []);
|
||||
|
||||
if (shouldCrash) {
|
||||
return {} as any;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button onClick={() => socket.reset()}>{t`Reset Socket`}</Button>
|
||||
<Button onClick={() => socket.simulateNetworkDisconnect()}>{t`Disconnect Socket`}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void MediaEngineStore.moveToAfkChannel();
|
||||
}}
|
||||
>
|
||||
{t`Force Move to AFK Channel`}
|
||||
</Button>
|
||||
<Button onClick={() => NewDeviceMonitoringStore.showTestModal()}>{t`Show New Device Modal`}</Button>
|
||||
<Button onClick={handleOpenCaptchaModal}>{t`Open Captcha Modal`}</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
ModalActionCreators.push(ModalActionCreators.modal(() => <KeyboardModeIntroModal />));
|
||||
}}
|
||||
>
|
||||
{t`Show Keyboard Mode Intro`}
|
||||
</Button>
|
||||
<Button onClick={handleOpenClaimAccountModal}>{t`Open Claim Account Modal`}</Button>
|
||||
<Button onClick={() => void handleTestBulkDelete()} submitting={isTestingBulkDelete} variant="danger-primary">
|
||||
{t`Test Bulk Delete (60s)`}
|
||||
</Button>
|
||||
<Button onClick={() => setShouldCrash(true)} variant="danger-primary">
|
||||
{t`Trigger React Crash`}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1rem;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cardInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fontName {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.langCode {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.fontFamily {
|
||||
font-family: monospace;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.sampleText {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.weightCard {
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.weightHeader {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.weightLabel {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weightValue {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.weightItalic {
|
||||
margin-top: 0.25rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.scaleList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.scaleItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.scaleSize {
|
||||
width: 5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.scaleSizeText {
|
||||
font-family: monospace;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.scaleLabel {
|
||||
width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.scaleLabelText {
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.scaleSample {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.styleGrid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.styleGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.styleGrid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.styleCard {
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.styleLabel {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.codeGrid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.codeGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.codeCard {
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.codeTitle {
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.codeLines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.multilingualCard {
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-secondary);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.multilingualList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.multilingualItem {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* 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 type React from 'react';
|
||||
import styles from './TypographyTab.module.css';
|
||||
|
||||
const fontSamples = [
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans',
|
||||
name: 'IBM Plex Sans',
|
||||
sample: 'The quick brown fox jumps over the lazy dog',
|
||||
lang: 'en',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans JP',
|
||||
name: 'IBM Plex Sans Japanese',
|
||||
sample: 'これは日本語のサンプルテキストです',
|
||||
lang: 'ja',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans KR',
|
||||
name: 'IBM Plex Sans Korean',
|
||||
sample: '이것은 한국어 샘플 텍스트입니다',
|
||||
lang: 'ko',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans SC',
|
||||
name: 'IBM Plex Sans Simplified Chinese',
|
||||
sample: '这是简体中文的示例文本',
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans TC',
|
||||
name: 'IBM Plex Sans Traditional Chinese',
|
||||
sample: '這是繁體中文的示例文本',
|
||||
lang: 'zh-TW',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans Arabic',
|
||||
name: 'IBM Plex Sans Arabic',
|
||||
sample: 'هذه عينة نصية باللغة العربية',
|
||||
lang: 'ar',
|
||||
rtl: true,
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans Hebrew',
|
||||
name: 'IBM Plex Sans Hebrew',
|
||||
sample: 'זוהי דוגמה לטקסט בעברית',
|
||||
lang: 'he',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans Devanagari',
|
||||
name: 'IBM Plex Sans Devanagari',
|
||||
sample: 'यह हिंदी का नमूना पाठ है',
|
||||
lang: 'hi',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans Thai',
|
||||
name: 'IBM Plex Sans Thai',
|
||||
sample: 'นี่คือข้อความตัวอย่างภาษาไทย',
|
||||
lang: 'th',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Sans Thai Looped',
|
||||
name: 'IBM Plex Sans Thai Looped',
|
||||
sample: 'นี่คือข้อความตัวอย่างภาษาไทยแบบ Loop',
|
||||
lang: 'th',
|
||||
},
|
||||
{
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
name: 'IBM Plex Mono',
|
||||
sample: 'const hello = "World"; function example() { return true; }',
|
||||
lang: 'en',
|
||||
mono: true,
|
||||
},
|
||||
];
|
||||
|
||||
const weightExamples = [
|
||||
{weight: 100, label: 'Thin', text: 'Delicate and light typography'},
|
||||
{weight: 200, label: 'Extra Light', text: 'Gentle and airy text display'},
|
||||
{weight: 300, label: 'Light', text: 'Soft and easy reading experience'},
|
||||
{weight: 400, label: 'Regular', text: 'Perfect for everyday content'},
|
||||
{weight: 450, label: 'Text', text: 'Optimized for longer reading passages'},
|
||||
{weight: 500, label: 'Medium', text: 'Slightly bolder emphasis'},
|
||||
{weight: 600, label: 'Semi Bold', text: 'Strong visual hierarchy'},
|
||||
{weight: 700, label: 'Bold', text: 'Powerful and attention-grabbing'},
|
||||
];
|
||||
|
||||
const scaleExamples = [
|
||||
{size: '12px', label: 'Caption', weight: 400},
|
||||
{size: '14px', label: 'Small', weight: 400},
|
||||
{size: '16px', label: 'Body', weight: 400},
|
||||
{size: '18px', label: 'Large', weight: 500},
|
||||
{size: '20px', label: 'Subtitle', weight: 500},
|
||||
{size: '24px', label: 'Heading', weight: 600},
|
||||
{size: '30px', label: 'Title', weight: 700},
|
||||
{size: '36px', label: 'Display', weight: 700},
|
||||
];
|
||||
|
||||
const contrastExamples = [
|
||||
{weight: 400, size: '16px', style: 'Normal'},
|
||||
{weight: 400, size: '16px', style: 'Italic'},
|
||||
{weight: 600, size: '16px', style: 'Semi Bold'},
|
||||
{weight: 600, size: '18px', style: 'Semi Bold Large'},
|
||||
{weight: 700, size: '20px', style: 'Bold Heading'},
|
||||
{weight: 700, size: '24px', style: 'Bold Title'},
|
||||
];
|
||||
|
||||
export const TypographyTabContent: React.FC = observer(() => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.heading}>
|
||||
<Trans>Typography Showcase</Trans>
|
||||
</h2>
|
||||
<p className={styles.description}>
|
||||
<Trans>
|
||||
Preview all available fonts, weights, and styles across different languages supported by Fluxer.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
<Trans>Language Support</Trans>
|
||||
</h3>
|
||||
<div className={styles.grid}>
|
||||
{fontSamples.map((font) => (
|
||||
<div key={font.fontFamily} className={styles.card}>
|
||||
<div className={styles.cardHeader}>
|
||||
<div className={styles.cardInfo}>
|
||||
<span className={styles.fontName}>{font.name}</span>
|
||||
<span className={styles.langCode}>{font.lang.toUpperCase()}</span>
|
||||
</div>
|
||||
<span className={styles.fontFamily}>{font.fontFamily}</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.sampleText}
|
||||
style={{
|
||||
fontFamily: font.mono ? 'var(--font-mono)' : `"${font.fontFamily}", var(--font-sans)`,
|
||||
fontSize: '16px',
|
||||
textAlign: font.rtl ? 'right' : 'left',
|
||||
}}
|
||||
lang={font.lang}
|
||||
dir={font.rtl ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{font.sample}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
<Trans>Font Weights</Trans>
|
||||
</h3>
|
||||
<div className={styles.codeGrid}>
|
||||
{weightExamples.map((example) => (
|
||||
<div key={example.weight} className={styles.weightCard}>
|
||||
<div className={styles.cardHeader}>
|
||||
<span className={styles.weightLabel}>{example.label}</span>
|
||||
<span className={styles.weightValue}>{example.weight}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: example.weight,
|
||||
fontFamily: 'var(--font-sans)',
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{example.text}
|
||||
</div>
|
||||
<div
|
||||
className={styles.weightItalic}
|
||||
style={{
|
||||
fontWeight: example.weight,
|
||||
fontFamily: 'var(--font-sans)',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
Italic style demonstration
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
<Trans>Type Scale</Trans>
|
||||
</h3>
|
||||
<div className={styles.scaleList}>
|
||||
{scaleExamples.map((example) => (
|
||||
<div key={example.size} className={styles.scaleItem}>
|
||||
<div className={styles.scaleSize}>
|
||||
<span className={styles.fontFamily}>{example.size}</span>
|
||||
</div>
|
||||
<div className={styles.scaleLabel}>
|
||||
<span className={styles.scaleLabelText}>{example.label}</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.scaleSample}
|
||||
style={{
|
||||
fontSize: example.size,
|
||||
fontWeight: example.weight,
|
||||
fontFamily: 'var(--font-sans)',
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
Typography scale demonstration
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
<Trans>Style Variations</Trans>
|
||||
</h3>
|
||||
<div className={styles.grid}>
|
||||
{contrastExamples.map((example, index) => (
|
||||
<div key={index} className={styles.weightCard}>
|
||||
<div className={styles.styleLabel}>{example.style}</div>
|
||||
<div
|
||||
className={example.style.includes('Italic') ? styles.italic : ''}
|
||||
style={{
|
||||
fontSize: example.size,
|
||||
fontWeight: example.weight,
|
||||
fontFamily: 'var(--font-sans)',
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
This text demonstrates {example.style.toLowerCase()} styling
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
<Trans>Code & Monospace</Trans>
|
||||
</h3>
|
||||
<div className={styles.codeGrid}>
|
||||
<div className={styles.weightCard}>
|
||||
<div className={styles.codeTitle}>Light Code</div>
|
||||
<div
|
||||
className={styles.codeLines}
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontWeight: 300,
|
||||
}}
|
||||
>
|
||||
<div>const example = "light";</div>
|
||||
<div>function demo() {'{'}</div>
|
||||
<div> return true;</div>
|
||||
<div>{'}'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.weightCard}>
|
||||
<div className={styles.codeTitle}>Regular Code</div>
|
||||
<div
|
||||
className={styles.codeLines}
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
<div>const example = "regular";</div>
|
||||
<div>function demo() {'{'}</div>
|
||||
<div> return true;</div>
|
||||
<div>{'}'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.weightCard}>
|
||||
<div className={styles.codeTitle}>Medium Code</div>
|
||||
<div
|
||||
className={styles.codeLines}
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div>const example = "medium";</div>
|
||||
<div>function demo() {'{'}</div>
|
||||
<div> return true;</div>
|
||||
<div>{'}'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.weightCard}>
|
||||
<div className={styles.codeTitle}>Bold Code</div>
|
||||
<div
|
||||
className={styles.codeLines}
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<div>const example = "bold";</div>
|
||||
<div>function demo() {'{'}</div>
|
||||
<div> return true;</div>
|
||||
<div>{'}'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.subheading}>
|
||||
<Trans>Multilingual Content</Trans>
|
||||
</h3>
|
||||
<div className={styles.multilingualCard}>
|
||||
<div className={styles.multilingualList} style={{fontFamily: 'var(--font-sans)', lineHeight: 1.6}}>
|
||||
<div className={styles.multilingualItem}>
|
||||
<strong>English:</strong> Welcome to Fluxer's typography showcase
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="ja">
|
||||
<strong>日本語:</strong> フラクサーのタイポグラフィショーケースへようこそ
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="ko">
|
||||
<strong>한국어:</strong> Fluxer의 타이포그래피 쇼케이스에 오신 것을 환영합니다
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="zh-CN">
|
||||
<strong>简体中文:</strong> 欢迎来到 Fluxer 的字体展示
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="zh-TW">
|
||||
<strong>繁體中文:</strong> 歡迎來到 Fluxer 的字體展示
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="ar" dir="rtl">
|
||||
<strong>العربية:</strong> مرحباً بك في عرض طباعة Fluxer
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="he">
|
||||
<strong>עברית:</strong> ברוכים הבאים לתצוגת הטיפוגרפיה של Fluxer
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="hi">
|
||||
<strong>हिंदी:</strong> Fluxer के टाइपोग्राफी शोकेस में आपका स्वागत है
|
||||
</div>
|
||||
<div className={styles.multilingualItem} lang="th">
|
||||
<strong>ไทย:</strong> ยินดีต้อนรับสู่การจัดแสดงพิมพ์ของ Fluxer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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 {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {SettingsSection} from '~/components/modals/shared/SettingsSection';
|
||||
import {SettingsTabContainer, SettingsTabContent} from '~/components/modals/shared/SettingsTabLayout';
|
||||
import ConnectionStore from '~/stores/ConnectionStore';
|
||||
import NagbarStore from '~/stores/NagbarStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {AccountPremiumTabContent} from './AccountPremiumTab';
|
||||
import {GeneralTabContent} from './GeneralTab';
|
||||
import {MockingTabContent} from './MockingTab';
|
||||
import {NagbarsTabContent} from './NagbarsTab';
|
||||
import {ToolsTabContent} from './ToolsTab';
|
||||
import {TypographyTabContent} from './TypographyTab';
|
||||
|
||||
const DeveloperOptionsTab: React.FC = observer(() => {
|
||||
const socket = ConnectionStore.socket;
|
||||
const nagbarState = NagbarStore;
|
||||
const user = UserStore.currentUser;
|
||||
|
||||
if (!(user && socket)) return null;
|
||||
|
||||
return (
|
||||
<SettingsTabContainer>
|
||||
<SettingsTabContent>
|
||||
<SettingsSection id="general" title={<Trans>General</Trans>}>
|
||||
<GeneralTabContent />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection id="account_premium" title={<Trans>Account & Premium</Trans>}>
|
||||
<AccountPremiumTabContent user={user} />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection id="mocking" title={<Trans>Mocking</Trans>}>
|
||||
<MockingTabContent />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection id="nagbars" title={<Trans>Nagbars</Trans>}>
|
||||
<NagbarsTabContent nagbarState={nagbarState} />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection id="tools" title={<Trans>Tools</Trans>}>
|
||||
<ToolsTabContent socket={socket} />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection id="typography" title={<Trans>Typography</Trans>}>
|
||||
<TypographyTabContent />
|
||||
</SettingsSection>
|
||||
</SettingsTabContent>
|
||||
</SettingsTabContainer>
|
||||
);
|
||||
});
|
||||
|
||||
export default DeveloperOptionsTab;
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 type {MessageDescriptor} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {Trans} from '@lingui/react/macro';
|
||||
import type React from 'react';
|
||||
import type {NagbarStore, NagbarToggleKey} from '~/stores/NagbarStore';
|
||||
|
||||
export type DismissibleNagbarKey = NagbarToggleKey;
|
||||
|
||||
export interface NagbarControlDefinition {
|
||||
key: string;
|
||||
label: React.ReactNode;
|
||||
forceKey: DismissibleNagbarKey;
|
||||
forceHideKey: NagbarToggleKey;
|
||||
resetKeys: Array<DismissibleNagbarKey>;
|
||||
status: (state: NagbarStore) => string;
|
||||
useActualDisabled?: (state: NagbarStore) => boolean;
|
||||
forceShowDisabled?: (state: NagbarStore) => boolean;
|
||||
forceHideDisabled?: (state: NagbarStore) => boolean;
|
||||
}
|
||||
|
||||
const FORCE_ENABLED_DESCRIPTOR = msg`Force enabled`;
|
||||
const FORCE_DISABLED_DESCRIPTOR = msg`Force disabled`;
|
||||
const USING_ACTUAL_ACCOUNT_STATE_DESCRIPTOR = msg`Using actual account state`;
|
||||
const USING_ACTUAL_VERIFICATION_STATE_DESCRIPTOR = msg`Using actual verification state`;
|
||||
const CURRENTLY_DISMISSED_DESCRIPTOR = msg`Currently dismissed`;
|
||||
const CURRENTLY_SHOWING_DESCRIPTOR = msg`Currently showing`;
|
||||
const USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR = msg`Using actual premium state`;
|
||||
const USING_ACTUAL_GIFT_INVENTORY_STATE_DESCRIPTOR = msg`Using actual gift inventory state`;
|
||||
const USING_ACTUAL_STATE_DESCRIPTOR = msg`Using actual state`;
|
||||
|
||||
export const getNagbarControls = (t: (message: MessageDescriptor) => string): Array<NagbarControlDefinition> => [
|
||||
{
|
||||
key: 'forceUnclaimedAccount',
|
||||
label: <Trans>Unclaimed Account Nagbar</Trans>,
|
||||
forceKey: 'forceUnclaimedAccount',
|
||||
forceHideKey: 'forceHideUnclaimedAccount',
|
||||
resetKeys: ['forceUnclaimedAccount'],
|
||||
status: (state) =>
|
||||
state.forceUnclaimedAccount
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHideUnclaimedAccount
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_ACCOUNT_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) => !state.forceUnclaimedAccount && !state.forceHideUnclaimedAccount,
|
||||
forceShowDisabled: (state) => state.forceUnclaimedAccount,
|
||||
forceHideDisabled: (state) => state.forceHideUnclaimedAccount,
|
||||
},
|
||||
{
|
||||
key: 'forceEmailVerification',
|
||||
label: <Trans>Email Verification Nagbar</Trans>,
|
||||
forceKey: 'forceEmailVerification',
|
||||
forceHideKey: 'forceHideEmailVerification',
|
||||
resetKeys: ['forceEmailVerification'],
|
||||
status: (state) =>
|
||||
state.forceEmailVerification
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHideEmailVerification
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_VERIFICATION_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) => !state.forceEmailVerification && !state.forceHideEmailVerification,
|
||||
forceShowDisabled: (state) => state.forceEmailVerification,
|
||||
forceHideDisabled: (state) => state.forceHideEmailVerification,
|
||||
},
|
||||
{
|
||||
key: 'forceDesktopNotification',
|
||||
label: <Trans>Desktop Notification Nagbar</Trans>,
|
||||
forceKey: 'forceDesktopNotification',
|
||||
forceHideKey: 'forceHideDesktopNotification',
|
||||
resetKeys: ['forceDesktopNotification', 'desktopNotificationDismissed'],
|
||||
status: (state) =>
|
||||
state.forceDesktopNotification
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHideDesktopNotification
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: state.desktopNotificationDismissed
|
||||
? t(CURRENTLY_DISMISSED_DESCRIPTOR)
|
||||
: t(CURRENTLY_SHOWING_DESCRIPTOR),
|
||||
useActualDisabled: (state) =>
|
||||
!state.forceDesktopNotification && !state.desktopNotificationDismissed && !state.forceHideDesktopNotification,
|
||||
forceShowDisabled: (state) => state.forceDesktopNotification,
|
||||
forceHideDisabled: (state) => state.forceHideDesktopNotification,
|
||||
},
|
||||
{
|
||||
key: 'forcePremiumGracePeriod',
|
||||
label: <Trans>Premium Grace Period Nagbar</Trans>,
|
||||
forceKey: 'forcePremiumGracePeriod',
|
||||
forceHideKey: 'forceHidePremiumGracePeriod',
|
||||
resetKeys: ['forcePremiumGracePeriod'],
|
||||
status: (state) =>
|
||||
state.forcePremiumGracePeriod
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHidePremiumGracePeriod
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) => !state.forcePremiumGracePeriod && !state.forceHidePremiumGracePeriod,
|
||||
forceShowDisabled: (state) => state.forcePremiumGracePeriod,
|
||||
forceHideDisabled: (state) => state.forceHidePremiumGracePeriod,
|
||||
},
|
||||
{
|
||||
key: 'forcePremiumExpired',
|
||||
label: <Trans>Premium Expired Nagbar</Trans>,
|
||||
forceKey: 'forcePremiumExpired',
|
||||
forceHideKey: 'forceHidePremiumExpired',
|
||||
resetKeys: ['forcePremiumExpired'],
|
||||
status: (state) =>
|
||||
state.forcePremiumExpired
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHidePremiumExpired
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) => !state.forcePremiumExpired && !state.forceHidePremiumExpired,
|
||||
forceShowDisabled: (state) => state.forcePremiumExpired,
|
||||
forceHideDisabled: (state) => state.forceHidePremiumExpired,
|
||||
},
|
||||
{
|
||||
key: 'forcePremiumOnboarding',
|
||||
label: <Trans>Premium Onboarding Nagbar</Trans>,
|
||||
forceKey: 'forcePremiumOnboarding',
|
||||
forceHideKey: 'forceHidePremiumOnboarding',
|
||||
resetKeys: ['forcePremiumOnboarding', 'premiumOnboardingDismissed'],
|
||||
status: (state) =>
|
||||
state.forcePremiumOnboarding
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHidePremiumOnboarding
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: state.premiumOnboardingDismissed
|
||||
? t(CURRENTLY_DISMISSED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_PREMIUM_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) =>
|
||||
!state.forcePremiumOnboarding && !state.premiumOnboardingDismissed && !state.forceHidePremiumOnboarding,
|
||||
forceShowDisabled: (state) => state.forcePremiumOnboarding,
|
||||
forceHideDisabled: (state) => state.forceHidePremiumOnboarding,
|
||||
},
|
||||
{
|
||||
key: 'forceGiftInventory',
|
||||
label: <Trans>Gift Inventory Nagbar</Trans>,
|
||||
forceKey: 'forceGiftInventory',
|
||||
forceHideKey: 'forceHideGiftInventory',
|
||||
resetKeys: ['forceGiftInventory', 'giftInventoryDismissed'],
|
||||
status: (state) =>
|
||||
state.forceGiftInventory
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHideGiftInventory
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: state.giftInventoryDismissed
|
||||
? t(CURRENTLY_DISMISSED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_GIFT_INVENTORY_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) =>
|
||||
!state.forceGiftInventory && !state.giftInventoryDismissed && !state.forceHideGiftInventory,
|
||||
forceShowDisabled: (state) => state.forceGiftInventory,
|
||||
forceHideDisabled: (state) => state.forceHideGiftInventory,
|
||||
},
|
||||
{
|
||||
key: 'forceInvitesDisabled',
|
||||
label: <Trans>Invites Disabled Nagbar</Trans>,
|
||||
forceKey: 'forceInvitesDisabled',
|
||||
forceHideKey: 'forceHideInvitesDisabled',
|
||||
resetKeys: ['forceInvitesDisabled'],
|
||||
status: (state) =>
|
||||
state.forceInvitesDisabled
|
||||
? t(FORCE_ENABLED_DESCRIPTOR)
|
||||
: state.forceHideInvitesDisabled
|
||||
? t(FORCE_DISABLED_DESCRIPTOR)
|
||||
: t(USING_ACTUAL_STATE_DESCRIPTOR),
|
||||
useActualDisabled: (state) => !state.forceInvitesDisabled && !state.forceHideInvitesDisabled,
|
||||
forceShowDisabled: (state) => state.forceInvitesDisabled,
|
||||
forceHideDisabled: (state) => state.forceHideInvitesDisabled,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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 type {MessageDescriptor} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import * as DeveloperOptionsActionCreators from '~/actions/DeveloperOptionsActionCreators';
|
||||
import {UserPremiumTypes} from '~/Constants';
|
||||
import type {SelectOption} from '~/components/form/Select';
|
||||
|
||||
interface PremiumScenarioSelectOption extends Omit<SelectOption<PremiumScenarioOption>, 'label'> {
|
||||
label: MessageDescriptor;
|
||||
}
|
||||
|
||||
export type PremiumScenarioOption =
|
||||
| 'none'
|
||||
| 'free_no_purchases'
|
||||
| 'free_with_history'
|
||||
| 'active_monthly'
|
||||
| 'active_monthly_cancelled'
|
||||
| 'active_yearly'
|
||||
| 'active_yearly_cancelled'
|
||||
| 'gift_active_no_history'
|
||||
| 'gift_active_with_history'
|
||||
| 'gift_expiring_soon'
|
||||
| 'gift_grace_period_active'
|
||||
| 'gift_expired_recent'
|
||||
| 'grace_period_active'
|
||||
| 'expired_recent'
|
||||
| 'expired_old'
|
||||
| 'visionary'
|
||||
| 'reset';
|
||||
|
||||
export const PREMIUM_SCENARIO_OPTIONS: ReadonlyArray<PremiumScenarioSelectOption> = [
|
||||
{value: 'none', label: msg`Select a scenario to apply...`},
|
||||
{value: 'free_no_purchases', label: msg`Free User (No Purchases)`},
|
||||
{value: 'free_with_history', label: msg`Free User (With Purchase History)`},
|
||||
{value: 'active_monthly', label: msg`Active Monthly Subscriber`},
|
||||
{value: 'active_monthly_cancelled', label: msg`Active Monthly Subscriber (Cancellation Scheduled)`},
|
||||
{value: 'active_yearly', label: msg`Active Yearly Subscriber`},
|
||||
{value: 'active_yearly_cancelled', label: msg`Active Yearly Subscriber (Cancellation Scheduled)`},
|
||||
{value: 'gift_active_no_history', label: msg`Gift – Active (No Purchase History)`},
|
||||
{value: 'gift_active_with_history', label: msg`Gift – Active (Has Purchase History)`},
|
||||
{value: 'gift_expiring_soon', label: msg`Gift – Expiring Soon`},
|
||||
{value: 'gift_grace_period_active', label: msg`Gift – Grace Period (Still Have Access)`},
|
||||
{value: 'gift_expired_recent', label: msg`Gift – Expired (Within 30 Days)`},
|
||||
{value: 'grace_period_active', label: msg`Grace Period (Still Have Access)`},
|
||||
{value: 'expired_recent', label: msg`Expired (Within 30 Days)`},
|
||||
{value: 'expired_old', label: msg`Expired (Over 30 Days Ago)`},
|
||||
{value: 'visionary', label: msg`Visionary (Lifetime #42)`},
|
||||
{value: 'reset', label: msg`Reset to Actual Values`},
|
||||
];
|
||||
|
||||
export const applyPremiumScenarioOption = (scenario: PremiumScenarioOption) => {
|
||||
if (scenario === 'none') return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
switch (scenario) {
|
||||
case 'free_no_purchases':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'free_with_history':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'active_monthly':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 15 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 15 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'monthly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'active_monthly_cancelled':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 20 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 10 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'monthly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', true);
|
||||
break;
|
||||
case 'active_yearly':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 180 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 185 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'active_yearly_cancelled':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 250 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 60 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', true);
|
||||
break;
|
||||
case 'gift_active_no_history':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 10 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 20 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'gift_active_with_history':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 5 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 60 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'gift_expiring_soon':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 25 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now + 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'gift_grace_period_active':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 31 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'gift_expired_recent':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', false);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 35 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 5 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'grace_period_active':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.SUBSCRIPTION);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 31 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'expired_recent':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 35 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 5 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'expired_old':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.NONE);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 65 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', new Date(now - 35 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', 'yearly');
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'visionary':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', UserPremiumTypes.LIFETIME);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', true);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', new Date(now - 365 * 86400000));
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', false);
|
||||
break;
|
||||
case 'reset':
|
||||
DeveloperOptionsActionCreators.updateOption('premiumTypeOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('hasEverPurchasedOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumSinceOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumUntilOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumBillingCycleOverride', null);
|
||||
DeveloperOptionsActionCreators.updateOption('premiumWillCancelOverride', null);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 type {MessageDescriptor} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import type {DeveloperOptionsState} from '~/stores/DeveloperOptionsStore';
|
||||
|
||||
export interface ToggleDef {
|
||||
key: keyof DeveloperOptionsState;
|
||||
label: MessageDescriptor;
|
||||
description?: MessageDescriptor;
|
||||
}
|
||||
|
||||
export interface ToggleGroup {
|
||||
title: MessageDescriptor;
|
||||
items: Array<ToggleDef>;
|
||||
}
|
||||
|
||||
export const getToggleGroups = (): Array<ToggleGroup> => [
|
||||
{
|
||||
title: msg`App State`,
|
||||
items: [
|
||||
{key: 'bypassSplashScreen', label: msg`Bypass Splash Screen`},
|
||||
{key: 'forceUpdateReady', label: msg`Force Update Ready`},
|
||||
{key: 'showMyselfTyping', label: msg`Show Myself Typing`},
|
||||
{
|
||||
key: 'selfHostedModeOverride',
|
||||
label: msg`Self-Hosted Mode Override`,
|
||||
description: msg`Enable self-hosted mode client-side (hides all premium/billing UI, grants everyone premium)`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: msg`UI Components`,
|
||||
items: [
|
||||
{key: 'forceGifPickerLoading', label: msg`Force GIF Picker Loading`},
|
||||
{
|
||||
key: 'forceShowVanityURLDisclaimer',
|
||||
label: msg`Force Show Vanity URL Disclaimer`,
|
||||
description: msg`Always show the vanity URL disclaimer warning in guild settings`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: msg`Networking & Performance`,
|
||||
items: [
|
||||
{key: 'slowMessageLoad', label: msg`Slow Message Load`},
|
||||
{key: 'slowMessageSend', label: msg`Slow Message Send`},
|
||||
{key: 'slowMessageEdit', label: msg`Slow Message Edit`},
|
||||
{key: 'slowAttachmentUpload', label: msg`Slow Attachment Upload`},
|
||||
{key: 'slowProfileLoad', label: msg`Slow Profile Load`},
|
||||
{
|
||||
key: 'forceProfileDataWarning',
|
||||
label: msg`Force Profile Data Warning`,
|
||||
description: msg`Always show the profile data warning indicator, even when the profile loads successfully`,
|
||||
},
|
||||
{key: 'forceFailUploads', label: msg`Force Fail Uploads`},
|
||||
{key: 'forceFailMessageSends', label: msg`Force Fail Message Sends`},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: msg`Features`,
|
||||
items: [
|
||||
{
|
||||
key: 'forceUnknownMessageType',
|
||||
label: msg`Force Unknown Message Type`,
|
||||
description: msg`Render all your messages as unknown message type`,
|
||||
},
|
||||
{
|
||||
key: 'forceShowVoiceConnection',
|
||||
label: msg`Force Show Voice Connection`,
|
||||
description: msg`Always display the voice connection status bar in mocked mode`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: msg`Logging & Diagnostics`,
|
||||
items: [{key: 'debugLogging', label: msg`Debug Logging`}],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user