import React, { useCallback, useEffect, useState } from 'react'; import { Box, Text, Switch, Button, Chip, Icon, Icons, color, config, Spinner } from 'folds'; import { IPusherRequest } from 'matrix-js-sdk'; import { useAtomValue, useSetAtom } from 'jotai'; import { useTimestampFormatter } from '../../../hooks/useTimestampFormatter'; import { NOTIFICATION_SOUND_MAP } from '../../../utils/notificationSounds'; 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 { isSnoozeActive, nextTimeAtHour, notificationSnoozeUntilAtom, SNOOZE_INDEFINITE, } from '../../../state/notificationSnooze'; import { formatFriendlyDateTime } from '../../../utils/datetimeInput'; 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', }, append: true, }); return; } await mx.setPusher({ pushkey: email, app_id: 'm.email', kind: null, } as unknown as IPusherRequest); }, [mx], ), ); const handleChange = (value: boolean) => { if (result && result.email) { setEnable(result.email, value).then(() => { refreshResult(); }); } }; return ( {result && !result.email && ( Your account does not have any email attached. )} {result && result.email && <>Send notification to your email. {`("${result.email}")`}} {result === null && ( Unexpected Error! )} {result === undefined && 'Send notification to your email.'} } after={ <> {setState.status !== AsyncStatus.Loading && typeof result === 'object' && result?.email && } {(setState.status === AsyncStatus.Loading || result === undefined) && ( )} } /> ); } 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', }; const SNOOZE_PRESETS: Array<{ label: string; resolve: (now: number) => number }> = [ { label: '30 minutes', resolve: (now) => now + 30 * 60_000 }, { label: '1 hour', resolve: (now) => now + 60 * 60_000 }, { label: '4 hours', resolve: (now) => now + 4 * 60 * 60_000 }, { label: 'Until 8 AM', resolve: (now) => nextTimeAtHour(8, now) }, { label: 'Until I resume', resolve: () => SNOOZE_INDEFINITE }, ]; // Cross-platform "pause notifications" — sets a snooze instant that the // notification gate (ClientNonUIFeatures) reads to suppress alerts + sounds. function PauseNotifications() { const { prefs } = useTimestampFormatter(); const snoozeUntil = useAtomValue(notificationSnoozeUntilAtom); const setSnoozeUntil = useSetAtom(notificationSnoozeUntilAtom); // While paused, tick so the status flips to "on" the moment the snooze lapses. const [, setTick] = useState(0); const active = isSnoozeActive(snoozeUntil); useEffect(() => { if (!active) return undefined; const id = setInterval(() => setTick((n) => n + 1), 30_000); return () => clearInterval(id); }, [active]); const status = !active ? 'Notifications are on.' : snoozeUntil >= SNOOZE_INDEFINITE ? 'Paused until you resume.' : `Paused until ${formatFriendlyDateTime(snoozeUntil, prefs)}.`; return ( {status}} after={ active ? ( ) : undefined } > {SNOOZE_PRESETS.map((preset) => ( setSnoozeUntil(preset.resolve(Date.now()))} > {preset.label} ))} ); } export function SystemNotification() { const notifPermission = usePermissionState('notifications', getNotificationState()); const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications'); const [isNotificationSounds, setIsNotificationSounds] = useSetting( settingsAtom, 'isNotificationSounds', ); 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'); const requestNotificationPermission = () => { window.Notification.requestPermission(); }; return ( System {'Notification' in window ? 'Notification permission is blocked. Please allow notification permission from browser address bar.' : 'Notifications are not supported by the system.'} ) : ( Show desktop notifications when messages arrive. ) } after={ notifPermission === 'prompt' ? ( ) : ( ) } /> } /> {isNotificationSounds && ( Message Sound {messageSoundId !== 'none' && ( )} Invite Sound {inviteSoundId !== 'none' && ( )} )} } /> {quietHoursEnabled && ( Start setQuietHoursStart(e.target.value)} aria-label="Quiet hours start time" style={selectStyle} /> End setQuietHoursEnd(e.target.value)} aria-label="Quiet hours end time" style={selectStyle} /> {(!quietHoursStart || !quietHoursEnd) && ( Set both a start and end time — quiet hours stay inactive until both are filled in. )} )} ); }