113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import React, { useCallback } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Text, Switch, Button, color, Spinner } from 'folds';
|
|
import { SettingTile } from '../../../components/setting-tile';
|
|
import { SettingsSection } from '../SettingsSection';
|
|
import { useSetting } from '../../../state/hooks/settings';
|
|
import { settingsAtom } from '../../../state/settings';
|
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|
import {
|
|
useRegisterPushNotifications,
|
|
useDisablePushNotifications,
|
|
usePushNotificationStatus,
|
|
usePushEnabled,
|
|
} from '../../../hooks/usePushNotifications';
|
|
|
|
const noop = (): void => undefined;
|
|
|
|
function PushNotification() {
|
|
const { t } = useTranslation();
|
|
const status = usePushNotificationStatus();
|
|
const register = useRegisterPushNotifications();
|
|
const disable = useDisablePushNotifications();
|
|
|
|
// Source of truth: setPushEnabled() in utils/push.ts fans out a custom event
|
|
// that this hook listens for, so the toggle reflects changes made elsewhere
|
|
// (e.g. the global PushPermissionPrompt on first login).
|
|
const enabled = usePushEnabled();
|
|
|
|
const [enableState, enable] = useAsyncCallback(
|
|
useCallback(async () => {
|
|
await register();
|
|
}, [register])
|
|
);
|
|
|
|
const [disableState, doDisable] = useAsyncCallback(
|
|
useCallback(async () => {
|
|
await disable();
|
|
}, [disable])
|
|
);
|
|
|
|
if (status === 'unavailable') return null;
|
|
|
|
const busy =
|
|
enableState.status === AsyncStatus.Loading || disableState.status === AsyncStatus.Loading;
|
|
|
|
let description: React.ReactNode = <span>{t('Settings.push_description')}</span>;
|
|
// A permission denial surfaces as both status='denied' AND enableState.error, but
|
|
// the status change is async on native (checkPermissions round-trip) while the
|
|
// error lands synchronously. Check the error reason first to avoid a flash of
|
|
// the generic push_error before the permission-specific message.
|
|
const permissionDenied =
|
|
status === 'denied' ||
|
|
(enableState.status === AsyncStatus.Error &&
|
|
(enableState.error as Error | undefined)?.message === 'permission_denied');
|
|
if (permissionDenied) {
|
|
description = (
|
|
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
|
|
{t('Settings.push_permission_blocked')}
|
|
</Text>
|
|
);
|
|
} else if (enableState.status === AsyncStatus.Error) {
|
|
description = (
|
|
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
|
|
{t('Settings.push_error')}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
// useAsyncCallback re-throws after recording the error state, so every call
|
|
// site needs .catch — otherwise expected flows like permission_denied turn
|
|
// into unhandled promise rejections.
|
|
let after: React.ReactNode = (
|
|
<Switch
|
|
disabled={status === 'denied'}
|
|
value={enabled}
|
|
onChange={(val) => (val ? enable().catch(noop) : doDisable().catch(noop))}
|
|
/>
|
|
);
|
|
if (busy) {
|
|
after = <Spinner variant="Secondary" />;
|
|
} else if (status === 'prompt' && !enabled) {
|
|
after = (
|
|
<Button size="300" radii="300" onClick={() => enable().catch(noop)}>
|
|
<Text size="B300">{t('Settings.enable')}</Text>
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SettingTile title={t('Settings.push_notifications')} description={description} after={after} />
|
|
);
|
|
}
|
|
|
|
export function SystemNotification() {
|
|
const { t } = useTranslation();
|
|
const [inviteSpamFilter, setInviteSpamFilter] = useSetting(settingsAtom, 'inviteSpamFilter');
|
|
// Gate the push row at the call site: PushNotification renders null when push
|
|
// is unavailable, and SettingsSection's `filter(Boolean)` can only drop
|
|
// JSX-level falsy children — a returned-null component would otherwise leave
|
|
// an empty bordered first row in the grouped panel.
|
|
const pushStatus = usePushNotificationStatus();
|
|
|
|
return (
|
|
<SettingsSection label={t('Settings.system')}>
|
|
{pushStatus !== 'unavailable' && <PushNotification />}
|
|
<SettingTile
|
|
title={t('Settings.invite_spam_filter')}
|
|
description={t('Settings.invite_spam_filter_desc')}
|
|
after={<Switch value={inviteSpamFilter} onChange={setInviteSpamFilter} />}
|
|
/>
|
|
</SettingsSection>
|
|
);
|
|
}
|