Files
cinny/src/app/features/settings/notifications/SystemNotification.tsx
T

318 lines
11 KiB
TypeScript
Raw Normal View History

2025-02-21 19:15:47 +11:00
import React, { useCallback } from 'react';
import { Box, Text, Switch, Button, color, config, Spinner } from 'folds';
2025-02-21 19:15:47 +11:00
import { IPusherRequest } from 'matrix-js-sdk';
import { NOTIFICATION_SOUND_MAP } from '../../../utils/notificationSounds';
2025-02-21 19:15:47 +11:00
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { getNotificationState, usePermissionState } from '../../../hooks/usePermission';
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
function EmailNotification() {
const mx = useMatrixClient();
const [result, refreshResult] = useEmailNotifications();
const [setState, setEnable] = useAsyncCallback(
useCallback(
async (email: string, enable: boolean) => {
if (enable) {
await mx.setPusher({
kind: 'email',
app_id: 'm.email',
pushkey: email,
app_display_name: 'Email Notifications',
device_display_name: email,
lang: 'en',
data: {
brand: 'Lotus Chat',
2025-02-21 19:15:47 +11:00
},
append: true,
});
return;
}
await mx.setPusher({
pushkey: email,
app_id: 'm.email',
kind: null,
} as unknown as IPusherRequest);
},
[mx],
),
2025-02-21 19:15:47 +11:00
);
const handleChange = (value: boolean) => {
if (result && result.email) {
setEnable(result.email, value).then(() => {
refreshResult();
});
}
};
return (
<SettingTile
title="Email Notification"
description={
<>
{result && !result.email && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
Your account does not have any email attached.
</Text>
)}
{result && result.email && <>Send notification to your email. {`("${result.email}")`}</>}
{result === null && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
Unexpected Error!
</Text>
)}
{result === undefined && 'Send notification to your email.'}
</>
}
after={
<>
{setState.status !== AsyncStatus.Loading &&
typeof result === 'object' &&
result?.email && <Switch value={result.enabled} onChange={handleChange} />}
{(setState.status === AsyncStatus.Loading || result === undefined) && (
<Spinner variant="Secondary" />
)}
</>
}
/>
);
}
const SOUND_OPTIONS: { label: string; value: 'notification' | 'invite' | 'call' | 'none' }[] = [
{ label: 'Default', value: 'notification' },
{ label: 'Ping', value: 'invite' },
{ label: 'Call', value: 'call' },
{ label: 'None', value: 'none' },
];
const INVITE_SOUND_OPTIONS: {
label: string;
value: 'notification' | 'invite' | 'call' | 'none';
}[] = [
{ label: 'Default', value: 'invite' },
{ label: 'Ping', value: 'notification' },
{ label: 'Call', value: 'call' },
{ label: 'None', value: 'none' },
];
function playPreview(soundId: string) {
const src = NOTIFICATION_SOUND_MAP[soundId];
if (!src) return;
new Audio(src).play().catch(() => undefined);
}
const selectStyle: React.CSSProperties = {
background: color.SurfaceVariant.Container,
border: `1px solid ${color.SurfaceVariant.ContainerLine}`,
borderRadius: config.radii.R300,
color: color.SurfaceVariant.OnContainer,
colorScheme: 'dark',
fontSize: '0.82rem',
padding: `${config.space.S100} ${config.space.S200}`,
cursor: 'pointer',
outline: 'none',
};
2025-02-21 19:15:47 +11:00
export function SystemNotification() {
const notifPermission = usePermissionState('notifications', getNotificationState());
const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
const [isNotificationSounds, setIsNotificationSounds] = useSetting(
settingsAtom,
'isNotificationSounds',
2025-02-21 19:15:47 +11:00
);
const [messageSoundId, setMessageSoundId] = useSetting(settingsAtom, 'messageSoundId');
const [inviteSoundId, setInviteSoundId] = useSetting(settingsAtom, 'inviteSoundId');
const [quietHoursEnabled, setQuietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled');
const [quietHoursStart, setQuietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
const [quietHoursEnd, setQuietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
2025-02-21 19:15:47 +11:00
const requestNotificationPermission = () => {
window.Notification.requestPermission();
};
return (
<Box direction="Column" gap="100">
<Text size="L400">System</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Desktop Notifications"
description={
notifPermission === 'denied' ? (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
{'Notification' in window
? 'Notification permission is blocked. Please allow notification permission from browser address bar.'
: 'Notifications are not supported by the system.'}
</Text>
) : (
<span>Show desktop notifications when message arrive.</span>
)
}
after={
notifPermission === 'prompt' ? (
<Button size="300" radii="300" onClick={requestNotificationPermission}>
<Text size="B300">Enable</Text>
</Button>
) : (
<Switch
disabled={notifPermission !== 'granted'}
value={showNotifications}
onChange={setShowNotifications}
/>
)
}
/>
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Notification Sound"
description="Play sound when new message arrive."
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
/>
{isNotificationSounds && (
<Box direction="Column" gap="300" style={{ paddingTop: config.space.S100 }}>
<Box alignItems="Center" gap="200">
<Text size="T300" style={{ flexGrow: 1, color: color.SurfaceVariant.OnContainer }}>
Message Sound
</Text>
<select
value={messageSoundId}
onChange={(e) =>
setMessageSoundId(e.target.value as 'notification' | 'invite' | 'call' | 'none')
}
aria-label="Message notification sound"
style={selectStyle}
>
{SOUND_OPTIONS.map((opt) => (
<option
key={opt.value}
value={opt.value}
style={{
background: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
}}
>
{opt.label}
</option>
))}
</select>
{messageSoundId !== 'none' && (
<Button
size="300"
radii="300"
variant="Secondary"
onClick={() => playPreview(messageSoundId)}
aria-label="Preview message sound"
>
<Text size="B300"></Text>
</Button>
)}
</Box>
<Box alignItems="Center" gap="200">
<Text size="T300" style={{ flexGrow: 1, color: color.SurfaceVariant.OnContainer }}>
Invite Sound
</Text>
<select
value={inviteSoundId}
onChange={(e) =>
setInviteSoundId(e.target.value as 'notification' | 'invite' | 'call' | 'none')
}
aria-label="Invite notification sound"
style={selectStyle}
>
{INVITE_SOUND_OPTIONS.map((opt) => (
<option
key={opt.value}
value={opt.value}
style={{
background: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
}}
>
{opt.label}
</option>
))}
</select>
{inviteSoundId !== 'none' && (
<Button
size="300"
radii="300"
variant="Secondary"
onClick={() => playPreview(inviteSoundId)}
aria-label="Preview invite sound"
>
<Text size="B300"></Text>
</Button>
)}
</Box>
</Box>
)}
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Quiet Hours"
description="Notifications are silenced between these times. Handles overnight (e.g. 11:00 PM 8:00 AM)."
after={<Switch value={quietHoursEnabled} onChange={setQuietHoursEnabled} />}
/>
{quietHoursEnabled && (
<Box direction="Column" gap="300" style={{ paddingTop: config.space.S100 }}>
<Box alignItems="Center" gap="200">
<Text size="T300" style={{ flexGrow: 1, color: color.SurfaceVariant.OnContainer }}>
Start
</Text>
<input
type="time"
value={quietHoursStart}
onChange={(e) => setQuietHoursStart(e.target.value)}
aria-label="Quiet hours start time"
style={selectStyle}
/>
</Box>
<Box alignItems="Center" gap="200">
<Text size="T300" style={{ flexGrow: 1, color: color.SurfaceVariant.OnContainer }}>
End
</Text>
<input
type="time"
value={quietHoursEnd}
onChange={(e) => setQuietHoursEnd(e.target.value)}
aria-label="Quiet hours end time"
style={selectStyle}
/>
</Box>
</Box>
)}
2025-02-21 19:15:47 +11:00
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<EmailNotification />
</SequenceCard>
</Box>
);
}