mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(push): gate relay delivery on operator consent (#2984)
This commit is contained in:
1 parent
336b8b7dcd
commit
01f53a168d
22 files changed
+1012
-26
No files matched your search
@@ -233,6 +233,15 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.noticeLink {
|
||||
align-self: flex-start;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--text-link);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.45;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.integrationFields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
MediaExpiryStep,
|
||||
type PremiumMode,
|
||||
PremiumStep,
|
||||
PushRelayConsentStep,
|
||||
type RegistrationMode,
|
||||
RegistrationStep,
|
||||
type ServiceAvailability,
|
||||
@@ -459,6 +460,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
youtube: false,
|
||||
bluesky: false,
|
||||
});
|
||||
const [pushRelayConsentAccepted, setPushRelayConsentAccepted] = useState(false);
|
||||
const [premiumMode, setPremiumMode] = useState<PremiumMode>('mirror');
|
||||
const [assets, setAssets] = useState<ReadonlyArray<BrandingAssetState>>(() =>
|
||||
BRANDING_ASSET_KINDS.map((kind) => ({kind, url: null, preview: null})),
|
||||
@@ -549,6 +551,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
clearStepNavigationLock();
|
||||
setIntegrationDraft({...DEFAULT_INTEGRATION_DRAFT});
|
||||
setMediaExpiryDraft({...DEFAULT_MEDIA_EXPIRY_DRAFT});
|
||||
setPushRelayConsentAccepted(false);
|
||||
setSmtpTesting(false);
|
||||
setSmtpTestResult(null);
|
||||
try {
|
||||
@@ -569,6 +572,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
setSingleCommunityEnabled(next.policy.single_community_enabled);
|
||||
setDirectMessagesDisabled(next.policy.direct_messages_disabled);
|
||||
setPremiumMode(next.policy.premium_mode);
|
||||
setPushRelayConsentAccepted(next.push_service_delivery.relay_consent_accepted);
|
||||
setServiceSelection({
|
||||
gif: next.policy.services_resolved.gif_enabled,
|
||||
youtube: next.policy.services_resolved.youtube_enabled,
|
||||
@@ -677,6 +681,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
if (step === 'branding') return !productNameError;
|
||||
if (step === 'community') return !singleCommunityNameError;
|
||||
if (step === 'media_expiry') return isMediaExpiryStepValid(mediaExpiryDraft);
|
||||
if (step === 'push_relay_consent') return true;
|
||||
const integrationKind = wizardStepToIntegrationKind(step);
|
||||
if (integrationKind) return isIntegrationStepValid(integrationKind, integrationDraft);
|
||||
return true;
|
||||
@@ -766,6 +771,10 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
const nextConfig = await updateInstanceConfig({
|
||||
integrations: buildIntegrationsPatch(integrationDraft),
|
||||
media: buildMediaPatch(mediaExpiryDraft),
|
||||
push_service_delivery:
|
||||
config.push_service_delivery.relay_consent_accepted === pushRelayConsentAccepted
|
||||
? undefined
|
||||
: {relay_consent_accepted: pushRelayConsentAccepted},
|
||||
registration: {mode: registrationMode},
|
||||
app_public: {
|
||||
branding: {
|
||||
@@ -804,6 +813,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
singleCommunityEnabled,
|
||||
singleCommunityNameTrimmed,
|
||||
directMessagesDisabled,
|
||||
pushRelayConsentAccepted,
|
||||
premiumMode,
|
||||
serviceAvailability,
|
||||
serviceSelection,
|
||||
@@ -972,6 +982,14 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
data-flx="app.setup.self-hosted-setup-wizard-gate.integration-step"
|
||||
/>
|
||||
)}
|
||||
{step === 'push_relay_consent' && (
|
||||
<PushRelayConsentStep
|
||||
accepted={pushRelayConsentAccepted}
|
||||
disabled={submitting}
|
||||
onChange={setPushRelayConsentAccepted}
|
||||
data-flx="app.setup.self-hosted-setup-wizard-gate.push-relay-consent-step"
|
||||
/>
|
||||
)}
|
||||
{step === 'services' && (
|
||||
<ServicesStep
|
||||
available={serviceAvailability}
|
||||
@@ -996,6 +1014,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
|
||||
singleCommunityEnabled={singleCommunityEnabled}
|
||||
directMessagesDisabled={directMessagesDisabled}
|
||||
attachmentExpiryEnabled={mediaExpiryDraft.enabled}
|
||||
pushRelayConsentAccepted={pushRelayConsentAccepted}
|
||||
premiumMode={premiumMode}
|
||||
submitError={submitError}
|
||||
data-flx="app.setup.self-hosted-setup-wizard-gate.finish-step"
|
||||
|
||||
@@ -17,6 +17,7 @@ export type WizardStep =
|
||||
| 'integration_captcha'
|
||||
| 'integration_email'
|
||||
| 'integration_bluesky'
|
||||
| 'push_relay_consent'
|
||||
| 'services'
|
||||
| 'premium'
|
||||
| 'finish';
|
||||
@@ -36,6 +37,7 @@ export const CONFIGURE_STEPS: ReadonlyArray<WizardStep> = [
|
||||
'integration_captcha',
|
||||
'integration_email',
|
||||
'integration_bluesky',
|
||||
'push_relay_consent',
|
||||
'services',
|
||||
'premium',
|
||||
'finish',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {ColorPickerField} from '@app/features/ui/components/form/ColorPickerFiel
|
||||
import {Input} from '@app/features/ui/components/form/FormInput';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import {Spinner} from '@app/features/ui/components/Spinner';
|
||||
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
|
||||
import {RadioGroup, type RadioOption} from '@app/features/ui/radio_group/RadioGroup';
|
||||
import {ThemeSelector} from '@app/features/user/components/modals/tabs/appearance_tab/theme/ThemeTabContent';
|
||||
import {LanguageSelector} from '@app/features/user/components/modals/tabs/LanguageTab';
|
||||
@@ -27,6 +28,8 @@ import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
|
||||
|
||||
const PUSH_RELAY_NOTICE_URL = 'https://fluxer.com/push-relay';
|
||||
|
||||
export type RegistrationMode = 'open' | 'approval' | 'closed';
|
||||
export type PremiumMode = 'mirror' | 'everyone';
|
||||
|
||||
@@ -263,6 +266,29 @@ const MEDIA_RENEW_WINDOW_LABEL_DESCRIPTOR = msg({
|
||||
comment: 'Label for attachment decay renewal window.',
|
||||
});
|
||||
|
||||
const PUSH_RELAY_TITLE_DESCRIPTOR = msg({
|
||||
message: 'Mobile push notifications',
|
||||
comment: 'Setup wizard push relay consent step title.',
|
||||
});
|
||||
const PUSH_RELAY_BODY_DESCRIPTOR = msg({
|
||||
message:
|
||||
"The official Fluxer mobile apps receive notifications through Fluxer's push relay, which hands them to Apple and Google. Self-hosted UnifiedPush and ntfy endpoints never reach the relay and need no agreement.",
|
||||
comment: 'Setup wizard push relay consent step body.',
|
||||
});
|
||||
const PUSH_RELAY_ACCEPT_LABEL_DESCRIPTOR = msg({
|
||||
message: 'Accept the push relay supplemental privacy notice',
|
||||
comment: 'Label for the push relay consent switch during setup.',
|
||||
});
|
||||
const PUSH_RELAY_ACCEPT_DESC_DESCRIPTOR = msg({
|
||||
message:
|
||||
'Leaving this off keeps the relay unused and drops notifications to the official mobile apps. You can accept it later in the admin panel.',
|
||||
comment: 'Description for the push relay consent switch during setup.',
|
||||
});
|
||||
const PUSH_RELAY_NOTICE_LINK_DESCRIPTOR = msg({
|
||||
message: 'Read the supplemental privacy notice',
|
||||
comment: 'Link to the push relay supplemental privacy notice shown during setup.',
|
||||
});
|
||||
|
||||
const SERVICES_TITLE_DESCRIPTOR = msg({
|
||||
message: 'Optional services',
|
||||
comment: 'Setup wizard optional services step title.',
|
||||
@@ -353,10 +379,22 @@ const SUMMARY_ATTACHMENT_EXPIRY_DESCRIPTOR = msg({
|
||||
message: 'Attachment expiration',
|
||||
comment: 'Summary row label for the attachment expiry choice in the setup wizard.',
|
||||
});
|
||||
const SUMMARY_PUSH_RELAY_DESCRIPTOR = msg({
|
||||
message: 'Push relay notice',
|
||||
comment: 'Summary row label for the push relay consent on the setup wizard finish step.',
|
||||
});
|
||||
const SUMMARY_PREMIUM_DESCRIPTOR = msg({
|
||||
message: 'Premium model',
|
||||
comment: 'Summary row label for the premium model in the setup wizard.',
|
||||
});
|
||||
const SUMMARY_ACCEPTED_DESCRIPTOR = msg({
|
||||
message: 'Accepted',
|
||||
comment: 'Summary value when the operator accepted the push relay notice.',
|
||||
});
|
||||
const SUMMARY_NOT_ACCEPTED_DESCRIPTOR = msg({
|
||||
message: 'Not accepted',
|
||||
comment: 'Summary value when the operator left the push relay notice unaccepted.',
|
||||
});
|
||||
const SUMMARY_ON_DESCRIPTOR = msg({
|
||||
message: 'Enabled',
|
||||
comment: 'Summary value when a setup option is enabled.',
|
||||
@@ -1531,6 +1569,40 @@ export const IntegrationStep = observer(
|
||||
},
|
||||
);
|
||||
|
||||
export const PushRelayConsentStep = observer(
|
||||
({accepted, disabled, onChange}: {accepted: boolean; disabled: boolean; onChange: (value: boolean) => void}) => {
|
||||
const {i18n} = useLingui();
|
||||
return (
|
||||
<section className={styles.step} data-flx="app.self-hosted-setup-wizard-gate.push-relay-consent-step">
|
||||
<StepHeader
|
||||
title={i18n._(PUSH_RELAY_TITLE_DESCRIPTOR)}
|
||||
body={i18n._(PUSH_RELAY_BODY_DESCRIPTOR)}
|
||||
data-flx="app.setup.setup-wizard-steps.push-relay-consent-step.step-header"
|
||||
/>
|
||||
<Switch
|
||||
label={i18n._(PUSH_RELAY_ACCEPT_LABEL_DESCRIPTOR)}
|
||||
description={i18n._(PUSH_RELAY_ACCEPT_DESC_DESCRIPTOR)}
|
||||
value={accepted}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
data-flx="app.self-hosted-setup-wizard-gate.push-relay-consent-switch"
|
||||
/>
|
||||
<FocusRing data-flx="app.setup.setup-wizard-steps.push-relay-consent-step.focus-ring">
|
||||
<a
|
||||
className={styles.noticeLink}
|
||||
href={PUSH_RELAY_NOTICE_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-flx="app.self-hosted-setup-wizard-gate.push-relay-notice-link"
|
||||
>
|
||||
{i18n._(PUSH_RELAY_NOTICE_LINK_DESCRIPTOR)}
|
||||
</a>
|
||||
</FocusRing>
|
||||
</section>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ServicesStep = observer(
|
||||
({
|
||||
available,
|
||||
@@ -1650,6 +1722,7 @@ export const FinishStep = observer(
|
||||
singleCommunityEnabled,
|
||||
directMessagesDisabled,
|
||||
attachmentExpiryEnabled,
|
||||
pushRelayConsentAccepted,
|
||||
premiumMode,
|
||||
submitError,
|
||||
}: {
|
||||
@@ -1658,6 +1731,7 @@ export const FinishStep = observer(
|
||||
singleCommunityEnabled: boolean;
|
||||
directMessagesDisabled: boolean;
|
||||
attachmentExpiryEnabled: boolean;
|
||||
pushRelayConsentAccepted: boolean;
|
||||
premiumMode: PremiumMode;
|
||||
submitError: string | null;
|
||||
}) => {
|
||||
@@ -1705,10 +1779,17 @@ export const FinishStep = observer(
|
||||
value={attachmentExpiryEnabled ? onLabel : offLabel}
|
||||
data-flx="app.setup.setup-wizard-steps.finish-step.summary-row--5"
|
||||
/>
|
||||
<SummaryRow
|
||||
label={i18n._(SUMMARY_PUSH_RELAY_DESCRIPTOR)}
|
||||
value={
|
||||
pushRelayConsentAccepted ? i18n._(SUMMARY_ACCEPTED_DESCRIPTOR) : i18n._(SUMMARY_NOT_ACCEPTED_DESCRIPTOR)
|
||||
}
|
||||
data-flx="app.setup.setup-wizard-steps.finish-step.summary-row--6"
|
||||
/>
|
||||
<SummaryRow
|
||||
label={i18n._(SUMMARY_PREMIUM_DESCRIPTOR)}
|
||||
value={premiumLabel}
|
||||
data-flx="app.setup.setup-wizard-steps.finish-step.summary-row--6"
|
||||
data-flx="app.setup.setup-wizard-steps.finish-step.summary-row--7"
|
||||
/>
|
||||
</div>
|
||||
{submitError && (
|
||||
|
||||
Reference in new issue
Block a user