import React, { ChangeEventHandler, FormEventHandler, useCallback, useEffect, useMemo, useState, } from 'react'; import { IPushRule, IPushRules, PushRuleKind } from 'matrix-js-sdk'; import { Box, Text, Button, Input, config, IconButton, Icons, Icon, Spinner, Switch } from 'folds'; import { SettingsSelect } from '../../../components/settings-select/SettingsSelect'; import { useAccountData } from '../../../hooks/useAccountData'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { getNotificationModeActions, NotificationMode, useNotificationModeActions, } from '../../../hooks/useNotificationMode'; import { NotificationModeSwitcher } from './NotificationModeSwitcher'; const RULE_LABELS: Record = { '.m.rule.master': 'Disable all notifications', '.m.rule.suppress_notices': 'Suppress bot notices', '.m.rule.invite_for_me': 'Invited to a room', '.m.rule.member_event': 'Member events (joins/leaves)', '.m.rule.is_user_mention': '@mention', '.m.rule.contains_display_name': 'Message contains my name', '.m.rule.is_room_mention': 'Room @mention', '.m.rule.tombstone': 'Room upgrade', '.m.rule.reaction': 'Reactions', '.m.rule.room_one_to_one': 'DM messages', '.m.rule.message': 'All messages', '.m.rule.encrypted': 'Encrypted messages', }; function getRuleLabel(ruleId: string): string { return RULE_LABELS[ruleId] ?? ruleId; } const MODE_LABELS: Record = { [NotificationMode.NotifyLoud]: 'Notify Loud', [NotificationMode.Notify]: 'Notify Silent', [NotificationMode.OFF]: 'Disable', }; const ADD_MODES: NotificationMode[] = [ NotificationMode.NotifyLoud, NotificationMode.Notify, NotificationMode.OFF, ]; type RuleEnableToggleProps = { kind: PushRuleKind; pushRule: IPushRule; }; function RuleEnableToggle({ kind, pushRule }: RuleEnableToggleProps) { const mx = useMatrixClient(); const [enabled, setEnabled] = useState(pushRule.enabled !== false); // Re-sync when the rule changes externally (e.g. toggled on another device → // account-data sync). The useState initializer only runs once, so without // this the Switch would show a stale value. useEffect(() => { setEnabled(pushRule.enabled !== false); }, [pushRule.enabled]); const [toggleState, toggle] = useAsyncCallback( useCallback( async (value: boolean) => { await mx.setPushRuleEnabled('global', kind, pushRule.rule_id, value); setEnabled(value); }, [mx, kind, pushRule.rule_id], ), ); const toggling = toggleState.status === AsyncStatus.Loading; return ( ); } type RuleDeleteButtonProps = { kind: PushRuleKind; pushRule: IPushRule; }; function RuleDeleteButton({ kind, pushRule }: RuleDeleteButtonProps) { const mx = useMatrixClient(); const [deleteState, doDelete] = useAsyncCallback( useCallback( () => mx.deletePushRule('global', kind, pushRule.rule_id), [mx, kind, pushRule.rule_id], ), ); const deleting = deleteState.status === AsyncStatus.Loading; return ( {deleting ? : } ); } type RuleModeSwitcherProps = { kind: PushRuleKind; pushRule: IPushRule; }; function RuleModeSwitcher({ kind, pushRule }: RuleModeSwitcherProps) { const mx = useMatrixClient(); const getModeActions = useNotificationModeActions(); const handleChange = useCallback( async (mode: NotificationMode) => { const actions = getModeActions(mode); await mx.setPushRuleActions('global', kind, pushRule.rule_id, actions); }, [mx, getModeActions, kind, pushRule.rule_id], ); return ; } type RuleRowProps = { kind: PushRuleKind; pushRule: IPushRule; custom: boolean; }; function RuleRow({ kind, pushRule, custom }: RuleRowProps) { return ( } after={ {custom && } } /> ); } type AddRuleFormProps = { kind: PushRuleKind.RoomSpecific | PushRuleKind.SenderSpecific; placeholder: string; label: string; }; function AddRuleForm({ kind, placeholder, label }: AddRuleFormProps) { const mx = useMatrixClient(); const [ruleId, setRuleId] = useState(''); const [mode, setMode] = useState(NotificationMode.Notify); const [addState, doAdd] = useAsyncCallback( useCallback( async (id: string, notifyMode: NotificationMode) => { const actions = getNotificationModeActions(notifyMode); await mx.addPushRule('global', kind, id, { actions }); setRuleId(''); }, [mx, kind], ), ); const adding = addState.status === AsyncStatus.Loading; const handleSubmit: FormEventHandler = (evt) => { evt.preventDefault(); if (adding) return; const trimmedId = ruleId.trim(); if (!trimmedId) return; doAdd(trimmedId, mode); }; const handleChange: ChangeEventHandler = (evt) => { setRuleId(evt.currentTarget.value); }; return ( {label} ({ value: m, label: MODE_LABELS[m] }))} onChange={setMode} aria-label="Notification mode" /> ); } type RuleSectionProps = { title: string; kind: PushRuleKind; rules: IPushRule[]; addForm?: React.ReactNode; }; function RuleSection({ title, kind, rules, addForm }: RuleSectionProps) { const [expanded, setExpanded] = useState(false); return ( setExpanded((v) => !v)} variant="Secondary" fill="Soft" size="300" radii="300" outlined before={ } > {expanded ? 'Collapse' : 'Expand'} } /> {expanded && addForm && {addForm}} {expanded && rules.length === 0 && !addForm && ( No rules configured. )} {expanded && rules.map((pushRule) => ( ))} ); } export function PushRuleEditor() { const pushRulesEvt = useAccountData(AccountDataEvent.PushRules); const pushRules = useMemo( () => pushRulesEvt?.getContent() ?? { global: {} }, [pushRulesEvt], ); const overrideRules = useMemo(() => pushRules.global[PushRuleKind.Override] ?? [], [pushRules]); const roomRules = useMemo(() => pushRules.global[PushRuleKind.RoomSpecific] ?? [], [pushRules]); const senderRules = useMemo( () => pushRules.global[PushRuleKind.SenderSpecific] ?? [], [pushRules], ); const underrideRules = useMemo(() => pushRules.global[PushRuleKind.Underride] ?? [], [pushRules]); return ( Advanced Push Rules } /> } /> ); }