mirror of
https://github.com/element-hq/element-web.git
synced 2024-12-16 10:41:36 +03:00
b31043b5eb
Follow-on from https://github.com/matrix-org/matrix-react-sdk/pull/6352 Fixes https://github.com/vector-im/element-web/issues/18048
647 lines
24 KiB
TypeScript
647 lines
24 KiB
TypeScript
/*
|
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import React from "react";
|
|
import Spinner from "../elements/Spinner";
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules";
|
|
import {
|
|
ContentRules,
|
|
IContentRules,
|
|
PushRuleVectorState,
|
|
VectorPushRulesDefinitions,
|
|
VectorState,
|
|
} from "../../../notifications";
|
|
import { _t, TranslatedString } from "../../../languageHandler";
|
|
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
|
import Modal from "../../../Modal";
|
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
|
import SdkConfig from "../../../SdkConfig";
|
|
import AccessibleButton from "../elements/AccessibleButton";
|
|
import TagComposer from "../elements/TagComposer";
|
|
import { objectClone } from "../../../utils/objects";
|
|
import { arrayDiff } from "../../../utils/arrays";
|
|
|
|
// TODO: this "view" component still has far too much application logic in it,
|
|
// which should be factored out to other files.
|
|
|
|
enum Phase {
|
|
Loading = "loading",
|
|
Ready = "ready",
|
|
Persisting = "persisting", // technically a meta-state for Ready, but whatever
|
|
Error = "error",
|
|
}
|
|
|
|
enum RuleClass {
|
|
Master = "master",
|
|
|
|
// The vector sections map approximately to UI sections
|
|
VectorGlobal = "vector_global",
|
|
VectorMentions = "vector_mentions",
|
|
VectorOther = "vector_other",
|
|
Other = "other", // unknown rules, essentially
|
|
}
|
|
|
|
const KEYWORD_RULE_ID = "_keywords"; // used as a placeholder "Rule ID" throughout this component
|
|
const KEYWORD_RULE_CATEGORY = RuleClass.VectorMentions;
|
|
|
|
// This array doesn't care about categories: it's just used for a simple sort
|
|
const RULE_DISPLAY_ORDER: string[] = [
|
|
// Global
|
|
RuleId.DM,
|
|
RuleId.EncryptedDM,
|
|
RuleId.Message,
|
|
RuleId.EncryptedMessage,
|
|
|
|
// Mentions
|
|
RuleId.ContainsDisplayName,
|
|
RuleId.ContainsUserName,
|
|
RuleId.AtRoomNotification,
|
|
|
|
// Other
|
|
RuleId.InviteToSelf,
|
|
RuleId.IncomingCall,
|
|
RuleId.SuppressNotices,
|
|
RuleId.Tombstone,
|
|
];
|
|
|
|
interface IVectorPushRule {
|
|
ruleId: RuleId | typeof KEYWORD_RULE_ID | string;
|
|
rule?: IAnnotatedPushRule;
|
|
description: TranslatedString | string;
|
|
vectorState: VectorState;
|
|
}
|
|
|
|
interface IProps {}
|
|
|
|
interface IState {
|
|
phase: Phase;
|
|
|
|
// Optional stuff is required when `phase === Ready`
|
|
masterPushRule?: IAnnotatedPushRule;
|
|
vectorKeywordRuleInfo?: IContentRules;
|
|
vectorPushRules?: {
|
|
[category in RuleClass]?: IVectorPushRule[];
|
|
};
|
|
pushers?: IPusher[];
|
|
threepids?: IThreepid[];
|
|
}
|
|
|
|
export default class Notifications extends React.PureComponent<IProps, IState> {
|
|
public constructor(props: IProps) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
phase: Phase.Loading,
|
|
};
|
|
}
|
|
|
|
private get isInhibited(): boolean {
|
|
// Caution: The master rule's enabled state is inverted from expectation. When
|
|
// the master rule is *enabled* it means all other rules are *disabled* (or
|
|
// inhibited). Conversely, when the master rule is *disabled* then all other rules
|
|
// are *enabled* (or operate fine).
|
|
return this.state.masterPushRule?.enabled;
|
|
}
|
|
|
|
public componentDidMount() {
|
|
// noinspection JSIgnoredPromiseFromCall
|
|
this.refreshFromServer();
|
|
}
|
|
|
|
private async refreshFromServer() {
|
|
try {
|
|
const newState = (await Promise.all([
|
|
this.refreshRules(),
|
|
this.refreshPushers(),
|
|
this.refreshThreepids(),
|
|
])).reduce((p, c) => Object.assign(c, p), {});
|
|
|
|
this.setState({
|
|
...newState,
|
|
phase: Phase.Ready,
|
|
});
|
|
} catch (e) {
|
|
console.error("Error setting up notifications for settings: ", e);
|
|
this.setState({ phase: Phase.Error });
|
|
}
|
|
}
|
|
|
|
private async refreshRules(): Promise<Partial<IState>> {
|
|
const ruleSets = await MatrixClientPeg.get().getPushRules();
|
|
|
|
const categories = {
|
|
[RuleId.Master]: RuleClass.Master,
|
|
|
|
[RuleId.DM]: RuleClass.VectorGlobal,
|
|
[RuleId.EncryptedDM]: RuleClass.VectorGlobal,
|
|
[RuleId.Message]: RuleClass.VectorGlobal,
|
|
[RuleId.EncryptedMessage]: RuleClass.VectorGlobal,
|
|
|
|
[RuleId.ContainsDisplayName]: RuleClass.VectorMentions,
|
|
[RuleId.ContainsUserName]: RuleClass.VectorMentions,
|
|
[RuleId.AtRoomNotification]: RuleClass.VectorMentions,
|
|
|
|
[RuleId.InviteToSelf]: RuleClass.VectorOther,
|
|
[RuleId.IncomingCall]: RuleClass.VectorOther,
|
|
[RuleId.SuppressNotices]: RuleClass.VectorOther,
|
|
[RuleId.Tombstone]: RuleClass.VectorOther,
|
|
|
|
// Everything maps to a generic "other" (unknown rule)
|
|
};
|
|
|
|
const defaultRules: {
|
|
[k in RuleClass]: IAnnotatedPushRule[];
|
|
} = {
|
|
[RuleClass.Master]: [],
|
|
[RuleClass.VectorGlobal]: [],
|
|
[RuleClass.VectorMentions]: [],
|
|
[RuleClass.VectorOther]: [],
|
|
[RuleClass.Other]: [],
|
|
};
|
|
|
|
for (const k in ruleSets.global) {
|
|
// noinspection JSUnfilteredForInLoop
|
|
const kind = k as PushRuleKind;
|
|
for (const r of ruleSets.global[kind]) {
|
|
const rule: IAnnotatedPushRule = Object.assign(r, { kind });
|
|
const category = categories[rule.rule_id] ?? RuleClass.Other;
|
|
|
|
if (rule.rule_id[0] === '.') {
|
|
defaultRules[category].push(rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
const preparedNewState: Partial<IState> = {};
|
|
if (defaultRules.master.length > 0) {
|
|
preparedNewState.masterPushRule = defaultRules.master[0];
|
|
} else {
|
|
// XXX: Can this even happen? How do we safely recover?
|
|
throw new Error("Failed to locate a master push rule");
|
|
}
|
|
|
|
// Parse keyword rules
|
|
preparedNewState.vectorKeywordRuleInfo = ContentRules.parseContentRules(ruleSets);
|
|
|
|
// Prepare rendering for all of our known rules
|
|
preparedNewState.vectorPushRules = {};
|
|
const vectorCategories = [RuleClass.VectorGlobal, RuleClass.VectorMentions, RuleClass.VectorOther];
|
|
for (const category of vectorCategories) {
|
|
preparedNewState.vectorPushRules[category] = [];
|
|
for (const rule of defaultRules[category]) {
|
|
const definition = VectorPushRulesDefinitions[rule.rule_id];
|
|
const vectorState = definition.ruleToVectorState(rule);
|
|
preparedNewState.vectorPushRules[category].push({
|
|
ruleId: rule.rule_id,
|
|
rule, vectorState,
|
|
description: _t(definition.description),
|
|
});
|
|
}
|
|
|
|
// Quickly sort the rules for display purposes
|
|
preparedNewState.vectorPushRules[category].sort((a, b) => {
|
|
let idxA = RULE_DISPLAY_ORDER.indexOf(a.ruleId);
|
|
let idxB = RULE_DISPLAY_ORDER.indexOf(b.ruleId);
|
|
|
|
// Assume unknown things go at the end
|
|
if (idxA < 0) idxA = RULE_DISPLAY_ORDER.length;
|
|
if (idxB < 0) idxB = RULE_DISPLAY_ORDER.length;
|
|
|
|
return idxA - idxB;
|
|
});
|
|
|
|
if (category === KEYWORD_RULE_CATEGORY) {
|
|
preparedNewState.vectorPushRules[category].push({
|
|
ruleId: KEYWORD_RULE_ID,
|
|
description: _t("Messages containing keywords"),
|
|
vectorState: preparedNewState.vectorKeywordRuleInfo.vectorState,
|
|
});
|
|
}
|
|
}
|
|
|
|
return preparedNewState;
|
|
}
|
|
|
|
private refreshPushers(): Promise<Partial<IState>> {
|
|
return MatrixClientPeg.get().getPushers();
|
|
}
|
|
|
|
private refreshThreepids(): Promise<Partial<IState>> {
|
|
return MatrixClientPeg.get().getThreePids();
|
|
}
|
|
|
|
private showSaveError() {
|
|
Modal.createTrackedDialog('Error saving notification preferences', '', ErrorDialog, {
|
|
title: _t('Error saving notification preferences'),
|
|
description: _t('An error occurred whilst saving your notification preferences.'),
|
|
});
|
|
}
|
|
|
|
private onMasterRuleChanged = async (checked: boolean) => {
|
|
this.setState({ phase: Phase.Persisting });
|
|
|
|
try {
|
|
const masterRule = this.state.masterPushRule;
|
|
await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked);
|
|
await this.refreshFromServer();
|
|
} catch (e) {
|
|
this.setState({ phase: Phase.Error });
|
|
console.error("Error updating master push rule:", e);
|
|
this.showSaveError();
|
|
}
|
|
};
|
|
|
|
private onEmailNotificationsChanged = async (email: string, checked: boolean) => {
|
|
this.setState({ phase: Phase.Persisting });
|
|
|
|
try {
|
|
if (checked) {
|
|
await MatrixClientPeg.get().setPusher({
|
|
kind: "email",
|
|
app_id: "m.email",
|
|
pushkey: email,
|
|
app_display_name: "Email Notifications",
|
|
device_display_name: email,
|
|
lang: navigator.language,
|
|
data: {
|
|
brand: SdkConfig.get().brand,
|
|
},
|
|
|
|
// We always append for email pushers since we don't want to stop other
|
|
// accounts notifying to the same email address
|
|
append: true,
|
|
});
|
|
} else {
|
|
const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email);
|
|
pusher.kind = null; // flag for delete
|
|
await MatrixClientPeg.get().setPusher(pusher);
|
|
}
|
|
|
|
await this.refreshFromServer();
|
|
} catch (e) {
|
|
this.setState({ phase: Phase.Error });
|
|
console.error("Error updating email pusher:", e);
|
|
this.showSaveError();
|
|
}
|
|
};
|
|
|
|
private onDesktopNotificationsChanged = async (checked: boolean) => {
|
|
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked);
|
|
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
|
};
|
|
|
|
private onDesktopShowBodyChanged = async (checked: boolean) => {
|
|
await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked);
|
|
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
|
};
|
|
|
|
private onAudioNotificationsChanged = async (checked: boolean) => {
|
|
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
|
this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue()
|
|
};
|
|
|
|
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState) => {
|
|
this.setState({ phase: Phase.Persisting });
|
|
|
|
try {
|
|
const cli = MatrixClientPeg.get();
|
|
if (rule.ruleId === KEYWORD_RULE_ID) {
|
|
// Update all the keywords
|
|
for (const rule of this.state.vectorKeywordRuleInfo.rules) {
|
|
let enabled: boolean;
|
|
let actions: PushRuleAction[];
|
|
if (checkedState === VectorState.On) {
|
|
if (rule.actions.length !== 1) { // XXX: Magic number
|
|
actions = PushRuleVectorState.actionsFor(checkedState);
|
|
}
|
|
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
|
|
enabled = true;
|
|
}
|
|
} else if (checkedState === VectorState.Loud) {
|
|
if (rule.actions.length !== 3) { // XXX: Magic number
|
|
actions = PushRuleVectorState.actionsFor(checkedState);
|
|
}
|
|
if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) {
|
|
enabled = true;
|
|
}
|
|
} else {
|
|
enabled = false;
|
|
}
|
|
|
|
if (actions) {
|
|
await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions);
|
|
}
|
|
if (enabled !== undefined) {
|
|
await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled);
|
|
}
|
|
}
|
|
} else {
|
|
const definition = VectorPushRulesDefinitions[rule.ruleId];
|
|
const actions = definition.vectorStateToActions[checkedState];
|
|
if (!actions) {
|
|
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false);
|
|
} else {
|
|
await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions);
|
|
await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true);
|
|
}
|
|
}
|
|
|
|
await this.refreshFromServer();
|
|
} catch (e) {
|
|
this.setState({ phase: Phase.Error });
|
|
console.error("Error updating push rule:", e);
|
|
this.showSaveError();
|
|
}
|
|
};
|
|
|
|
private onClearNotificationsClicked = () => {
|
|
MatrixClientPeg.get().getRooms().forEach(r => {
|
|
if (r.getUnreadNotificationCount() > 0) {
|
|
const events = r.getLiveTimeline().getEvents();
|
|
if (events.length) {
|
|
// noinspection JSIgnoredPromiseFromCall
|
|
MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) {
|
|
try {
|
|
// De-duplicate and remove empties
|
|
keywords = Array.from(new Set(keywords)).filter(k => !!k);
|
|
const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k);
|
|
|
|
// Note: Technically because of the UI interaction (at the time of writing), the diff
|
|
// will only ever be +/-1 so we don't really have to worry about efficiently handling
|
|
// tons of keyword changes.
|
|
|
|
const diff = arrayDiff(oldKeywords, keywords);
|
|
|
|
for (const word of diff.removed) {
|
|
for (const rule of originalRules.filter(r => r.pattern === word)) {
|
|
await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id);
|
|
}
|
|
}
|
|
|
|
let ruleVectorState = this.state.vectorKeywordRuleInfo.vectorState;
|
|
if (ruleVectorState === VectorState.Off) {
|
|
// When the current global keywords rule is OFF, we need to look at
|
|
// the flavor of existing rules to apply the same actions
|
|
// when creating the new rule.
|
|
if (originalRules.length) {
|
|
ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]);
|
|
} else {
|
|
ruleVectorState = VectorState.On; // default
|
|
}
|
|
}
|
|
const kind = PushRuleKind.ContentSpecific;
|
|
for (const word of diff.added) {
|
|
await MatrixClientPeg.get().addPushRule('global', kind, word, {
|
|
actions: PushRuleVectorState.actionsFor(ruleVectorState),
|
|
pattern: word,
|
|
});
|
|
if (ruleVectorState === VectorState.Off) {
|
|
await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false);
|
|
}
|
|
}
|
|
|
|
await this.refreshFromServer();
|
|
} catch (e) {
|
|
this.setState({ phase: Phase.Error });
|
|
console.error("Error updating keyword push rules:", e);
|
|
this.showSaveError();
|
|
}
|
|
}
|
|
|
|
private onKeywordAdd = (keyword: string) => {
|
|
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
|
|
|
// We add the keyword immediately as a sort of local echo effect
|
|
this.setState({
|
|
phase: Phase.Persisting,
|
|
vectorKeywordRuleInfo: {
|
|
...this.state.vectorKeywordRuleInfo,
|
|
rules: [
|
|
...this.state.vectorKeywordRuleInfo.rules,
|
|
|
|
// XXX: Horrible assumption that we don't need the remaining fields
|
|
{ pattern: keyword } as IAnnotatedPushRule,
|
|
],
|
|
},
|
|
}, async () => {
|
|
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
|
|
});
|
|
};
|
|
|
|
private onKeywordRemove = (keyword: string) => {
|
|
const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules);
|
|
|
|
// We remove the keyword immediately as a sort of local echo effect
|
|
this.setState({
|
|
phase: Phase.Persisting,
|
|
vectorKeywordRuleInfo: {
|
|
...this.state.vectorKeywordRuleInfo,
|
|
rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword),
|
|
},
|
|
}, async () => {
|
|
await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules);
|
|
});
|
|
};
|
|
|
|
private renderTopSection() {
|
|
const masterSwitch = <LabelledToggleSwitch
|
|
value={!this.isInhibited}
|
|
label={_t("Enable for this account")}
|
|
onChange={this.onMasterRuleChanged}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
/>;
|
|
|
|
// If all the rules are inhibited, don't show anything.
|
|
if (this.isInhibited) {
|
|
return masterSwitch;
|
|
}
|
|
|
|
const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email)
|
|
.map(e => <LabelledToggleSwitch
|
|
key={e.address}
|
|
value={this.state.pushers.some(p => p.kind === "email" && p.pushkey === e.address)}
|
|
label={_t("Enable email notifications for %(email)s", { email: e.address })}
|
|
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
/>);
|
|
|
|
return <>
|
|
{ masterSwitch }
|
|
|
|
<LabelledToggleSwitch
|
|
value={SettingsStore.getValue("notificationsEnabled")}
|
|
onChange={this.onDesktopNotificationsChanged}
|
|
label={_t('Enable desktop notifications for this session')}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
/>
|
|
|
|
<LabelledToggleSwitch
|
|
value={SettingsStore.getValue("notificationBodyEnabled")}
|
|
onChange={this.onDesktopShowBodyChanged}
|
|
label={_t('Show message in desktop notification')}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
/>
|
|
|
|
<LabelledToggleSwitch
|
|
value={SettingsStore.getValue("audioNotificationsEnabled")}
|
|
onChange={this.onAudioNotificationsChanged}
|
|
label={_t('Enable audible notifications for this session')}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
/>
|
|
|
|
{ emailSwitches }
|
|
</>;
|
|
}
|
|
|
|
private renderCategory(category: RuleClass) {
|
|
if (category !== RuleClass.VectorOther && this.isInhibited) {
|
|
return null; // nothing to show for the section
|
|
}
|
|
|
|
let clearNotifsButton: JSX.Element;
|
|
if (
|
|
category === RuleClass.VectorOther
|
|
&& MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)
|
|
) {
|
|
clearNotifsButton = <AccessibleButton
|
|
onClick={this.onClearNotificationsClicked}
|
|
kind='danger'
|
|
className='mx_UserNotifSettings_clearNotifsButton'
|
|
>{ _t("Clear notifications") }</AccessibleButton>;
|
|
}
|
|
|
|
if (category === RuleClass.VectorOther && this.isInhibited) {
|
|
// only render the utility buttons (if needed)
|
|
if (clearNotifsButton) {
|
|
return <div className='mx_UserNotifSettings_floatingSection'>
|
|
<div>{ _t("Other") }</div>
|
|
{ clearNotifsButton }
|
|
</div>;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
let keywordComposer: JSX.Element;
|
|
if (category === RuleClass.VectorMentions) {
|
|
keywordComposer = <TagComposer
|
|
tags={this.state.vectorKeywordRuleInfo?.rules.map(r => r.pattern)}
|
|
onAdd={this.onKeywordAdd}
|
|
onRemove={this.onKeywordRemove}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
label={_t("Keyword")}
|
|
placeholder={_t("New keyword")}
|
|
/>;
|
|
}
|
|
|
|
const makeRadio = (r: IVectorPushRule, s: VectorState) => (
|
|
<StyledRadioButton
|
|
key={r.ruleId}
|
|
name={r.ruleId}
|
|
checked={r.vectorState === s}
|
|
onChange={this.onRadioChecked.bind(this, r, s)}
|
|
disabled={this.state.phase === Phase.Persisting}
|
|
/>
|
|
);
|
|
|
|
const rows = this.state.vectorPushRules[category].map(r => <tr key={category + r.ruleId}>
|
|
<td>{ r.description }</td>
|
|
<td>{ makeRadio(r, VectorState.Off) }</td>
|
|
<td>{ makeRadio(r, VectorState.On) }</td>
|
|
<td>{ makeRadio(r, VectorState.Loud) }</td>
|
|
</tr>);
|
|
|
|
let sectionName: TranslatedString;
|
|
switch (category) {
|
|
case RuleClass.VectorGlobal:
|
|
sectionName = _t("Global");
|
|
break;
|
|
case RuleClass.VectorMentions:
|
|
sectionName = _t("Mentions & keywords");
|
|
break;
|
|
case RuleClass.VectorOther:
|
|
sectionName = _t("Other");
|
|
break;
|
|
default:
|
|
throw new Error("Developer error: Unnamed notifications section: " + category);
|
|
}
|
|
|
|
return <>
|
|
<table className='mx_UserNotifSettings_pushRulesTable'>
|
|
<thead>
|
|
<tr>
|
|
<th>{ sectionName }</th>
|
|
<th>{ _t("Off") }</th>
|
|
<th>{ _t("On") }</th>
|
|
<th>{ _t("Noisy") }</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{ rows }
|
|
</tbody>
|
|
</table>
|
|
{ clearNotifsButton }
|
|
{ keywordComposer }
|
|
</>;
|
|
}
|
|
|
|
private renderTargets() {
|
|
if (this.isInhibited) return null; // no targets if there's no notifications
|
|
|
|
const rows = this.state.pushers.map(p => <tr key={p.kind+p.pushkey}>
|
|
<td>{ p.app_display_name }</td>
|
|
<td>{ p.device_display_name }</td>
|
|
</tr>);
|
|
|
|
if (!rows.length) return null; // no targets to show
|
|
|
|
return <div className='mx_UserNotifSettings_floatingSection'>
|
|
<div>{ _t("Notification targets") }</div>
|
|
<table>
|
|
<tbody>
|
|
{ rows }
|
|
</tbody>
|
|
</table>
|
|
</div>;
|
|
}
|
|
|
|
public render() {
|
|
if (this.state.phase === Phase.Loading) {
|
|
// Ends up default centered
|
|
return <Spinner />;
|
|
} else if (this.state.phase === Phase.Error) {
|
|
return <p>{ _t("There was an error loading your notification settings.") }</p>;
|
|
}
|
|
|
|
return <div className='mx_UserNotifSettings'>
|
|
{ this.renderTopSection() }
|
|
{ this.renderCategory(RuleClass.VectorGlobal) }
|
|
{ this.renderCategory(RuleClass.VectorMentions) }
|
|
{ this.renderCategory(RuleClass.VectorOther) }
|
|
{ this.renderTargets() }
|
|
</div>;
|
|
}
|
|
}
|