2025-02-21 19:15:47 +11:00
|
|
|
|
import React, { useCallback } from 'react';
|
2026-06-03 19:41:02 -04:00
|
|
|
|
import { Box, Text, Switch, Button, color, config, Spinner } from 'folds';
|
2025-02-21 19:15:47 +11:00
|
|
|
|
import { IPusherRequest } from 'matrix-js-sdk';
|
2026-06-03 19:41:02 -04:00
|
|
|
|
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: {
|
2026-05-13 22:22:06 -04:00
|
|
|
|
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);
|
|
|
|
|
|
},
|
2026-05-21 23:30:50 -04:00
|
|
|
|
[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" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 19:41:02 -04:00
|
|
|
|
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,
|
2026-05-21 23:30:50 -04:00
|
|
|
|
'isNotificationSounds',
|
2025-02-21 19:15:47 +11:00
|
|
|
|
);
|
2026-06-03 19:41:02 -04: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} />}
|
|
|
|
|
|
/>
|
2026-06-03 19:41:02 -04:00
|
|
|
|
{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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|