mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 10:15:43 +03:00
Merge pull request #6352 from matrix-org/travis/notifications-2
Notification settings UI refresh
This commit is contained in:
commit
2089127dad
17 changed files with 1039 additions and 1273 deletions
|
@ -149,6 +149,7 @@
|
||||||
@import "./views/elements/_StyledCheckbox.scss";
|
@import "./views/elements/_StyledCheckbox.scss";
|
||||||
@import "./views/elements/_StyledRadioButton.scss";
|
@import "./views/elements/_StyledRadioButton.scss";
|
||||||
@import "./views/elements/_SyntaxHighlight.scss";
|
@import "./views/elements/_SyntaxHighlight.scss";
|
||||||
|
@import "./views/elements/_TagComposer.scss";
|
||||||
@import "./views/elements/_TextWithTooltip.scss";
|
@import "./views/elements/_TextWithTooltip.scss";
|
||||||
@import "./views/elements/_ToggleSwitch.scss";
|
@import "./views/elements/_ToggleSwitch.scss";
|
||||||
@import "./views/elements/_Tooltip.scss";
|
@import "./views/elements/_Tooltip.scss";
|
||||||
|
@ -263,9 +264,9 @@
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
|
@import "./views/voip/_CallPreview.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
@import "./views/voip/_CallPreview.scss";
|
|
||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
@import "./views/voip/_DialPadModal.scss";
|
@import "./views/voip/_DialPadModal.scss";
|
||||||
|
|
77
res/css/views/elements/_TagComposer.scss
Normal file
77
res/css/views/elements/_TagComposer.scss
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_TagComposer {
|
||||||
|
.mx_TagComposer_input {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.mx_Field {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0; // override from field styles
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
min-width: 70px;
|
||||||
|
padding: 0; // override from button styles
|
||||||
|
margin-left: 16px; // distance from <Field>
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Field, .mx_Field input, .mx_AccessibleButton {
|
||||||
|
// So they look related to each other by feeling the same
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TagComposer_tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 12px; // this plus 12px from the tags makes 24px from the input
|
||||||
|
|
||||||
|
.mx_TagComposer_tag {
|
||||||
|
padding: 6px 8px 8px 12px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
// Cheaty way to get an opacified variable colour background
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
opacity: 0.15;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
// Pass through the pointer otherwise we have effectively put a whole div
|
||||||
|
// on top of the component, which makes it hard to interact with buttons.
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
background-image: url('$(res)/img/subtract.svg');
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,82 +14,79 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_UserNotifSettings_tableRow {
|
.mx_UserNotifSettings {
|
||||||
display: table-row;
|
color: $primary-fg-color; // override from default settings page styles
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_inputCell {
|
.mx_UserNotifSettings_pushRulesTable {
|
||||||
display: table-cell;
|
width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches
|
||||||
padding-bottom: 8px;
|
table-layout: fixed;
|
||||||
padding-right: 8px;
|
border-collapse: collapse;
|
||||||
width: 16px;
|
border-spacing: 0;
|
||||||
}
|
margin-top: 40px;
|
||||||
|
|
||||||
.mx_UserNotifSettings_labelCell {
|
tr > th {
|
||||||
padding-bottom: 8px;
|
font-weight: $font-semi-bold;
|
||||||
width: 400px;
|
}
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTableWrapper {
|
tr > th:first-child {
|
||||||
padding-bottom: 8px;
|
text-align: left;
|
||||||
}
|
font-size: $font-18px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable {
|
tr > th:nth-child(n + 2) {
|
||||||
width: 100%;
|
color: $secondary-fg-color;
|
||||||
table-layout: fixed;
|
font-size: $font-12px;
|
||||||
}
|
vertical-align: middle;
|
||||||
|
width: 66px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable thead {
|
tr > td:nth-child(n + 2) {
|
||||||
font-weight: bold;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable tbody th {
|
tr > td {
|
||||||
font-weight: 400;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_pushRulesTable tbody th:first-child {
|
// Override StyledRadioButton default styles
|
||||||
text-align: left;
|
.mx_RadioButton {
|
||||||
}
|
justify-content: center;
|
||||||
|
|
||||||
.mx_UserNotifSettings_keywords {
|
.mx_RadioButton_content {
|
||||||
cursor: pointer;
|
display: none;
|
||||||
color: $accent-color;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_devicesTable td {
|
.mx_RadioButton_spacer {
|
||||||
padding-left: 20px;
|
display: none;
|
||||||
padding-right: 20px;
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserNotifSettings_notifTable {
|
.mx_UserNotifSettings_floatingSection {
|
||||||
display: table;
|
margin-top: 40px;
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserNotifSettings_notifTable .mx_Spinner {
|
& > div:first-child { // section header
|
||||||
position: absolute;
|
font-size: $font-18px;
|
||||||
}
|
font-weight: $font-semi-bold;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_NotificationSound_soundUpload {
|
> table {
|
||||||
display: none;
|
border-collapse: collapse;
|
||||||
}
|
border-spacing: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
.mx_NotificationSound_browse {
|
tr > td:first-child {
|
||||||
color: $accent-color;
|
// Just for a bit of spacing
|
||||||
border: 1px solid $accent-color;
|
padding-right: 8px;
|
||||||
background-color: transparent;
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_NotificationSound_save {
|
.mx_UserNotifSettings_clearNotifsButton {
|
||||||
margin-left: 5px;
|
margin-top: 8px;
|
||||||
color: white;
|
}
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_NotificationSound_resetSound {
|
.mx_TagComposer {
|
||||||
margin-top: 5px;
|
margin-top: 35px; // lots of distance from the last line of the table
|
||||||
color: white;
|
}
|
||||||
border: $warning-color;
|
|
||||||
background-color: $warning-color;
|
|
||||||
}
|
}
|
||||||
|
|
3
res/img/subtract.svg
Normal file
3
res/img/subtract.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58167 12.4183 0 8 0C3.58173 0 0 3.58167 0 8C0 12.4183 3.58173 16 8 16ZM3.96967 5.0304L6.93933 8L3.96967 10.9697L5.03033 12.0304L8 9.06067L10.9697 12.0304L12.0303 10.9697L9.06067 8L12.0303 5.0304L10.9697 3.96973L8 6.93945L5.03033 3.96973L3.96967 5.0304Z" fill="#8D97A5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 461 B |
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2019 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 PropTypes from "prop-types";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
const Spinner = ({ w = 32, h = 32, message }) => (
|
|
||||||
<div className="mx_Spinner">
|
|
||||||
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
|
||||||
<div
|
|
||||||
className="mx_Spinner_icon"
|
|
||||||
style={{ width: w, height: h }}
|
|
||||||
aria-label={_t("Loading...")}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
Spinner.propTypes = {
|
|
||||||
w: PropTypes.number,
|
|
||||||
h: PropTypes.number,
|
|
||||||
message: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Spinner;
|
|
45
src/components/views/elements/Spinner.tsx
Normal file
45
src/components/views/elements/Spinner.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015-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 { _t } from "../../../languageHandler";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Spinner extends React.PureComponent<IProps> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
w: 32,
|
||||||
|
h: 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { w, h, message } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="mx_Spinner">
|
||||||
|
{ message && <React.Fragment><div className="mx_Spinner_Msg">{ message }</div> </React.Fragment> }
|
||||||
|
<div
|
||||||
|
className="mx_Spinner_icon"
|
||||||
|
style={{ width: w, height: h }}
|
||||||
|
aria-label={_t("Loading...")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
91
src/components/views/elements/TagComposer.tsx
Normal file
91
src/components/views/elements/TagComposer.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
Copyright 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, { ChangeEvent, FormEvent } from "react";
|
||||||
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Field from "./Field";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
tags: string[];
|
||||||
|
onAdd: (tag: string) => void;
|
||||||
|
onRemove: (tag: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
newTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple, controlled, composer for entering string tags. Contains a simple
|
||||||
|
* input, add button, and per-tag remove button.
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.elements.TagComposer")
|
||||||
|
export default class TagComposer extends React.PureComponent<IProps, IState> {
|
||||||
|
public constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
newTag: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onInputChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ newTag: ev.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAdd = (ev: FormEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (!this.state.newTag) return;
|
||||||
|
|
||||||
|
this.props.onAdd(this.state.newTag);
|
||||||
|
this.setState({ newTag: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRemove(tag: string) {
|
||||||
|
// We probably don't need to proxy this, but for
|
||||||
|
// sanity of `this` we'll do so anyways.
|
||||||
|
this.props.onRemove(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <div className='mx_TagComposer'>
|
||||||
|
<form className='mx_TagComposer_input' onSubmit={this.onAdd}>
|
||||||
|
<Field
|
||||||
|
value={this.state.newTag}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
label={this.props.label || _t("Keyword")}
|
||||||
|
placeholder={this.props.placeholder || _t("New keyword")}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<AccessibleButton onClick={this.onAdd} kind='primary' disabled={this.props.disabled}>
|
||||||
|
{ _t("Add") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</form>
|
||||||
|
<div className='mx_TagComposer_tags'>
|
||||||
|
{ this.props.tags.map((t, i) => (<div className='mx_TagComposer_tag' key={i}>
|
||||||
|
<span>{ t }</span>
|
||||||
|
<AccessibleButton onClick={this.onRemove.bind(this, t)} disabled={this.props.disabled} />
|
||||||
|
</div>)) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -419,7 +419,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
>
|
>
|
||||||
<IconizedContextMenuOptionList first>
|
<IconizedContextMenuOptionList first>
|
||||||
<IconizedContextMenuRadio
|
<IconizedContextMenuRadio
|
||||||
label={_t("Use default")}
|
label={_t("Global")}
|
||||||
active={state === ALL_MESSAGES}
|
active={state === ALL_MESSAGES}
|
||||||
iconClassName="mx_RoomTile_iconBell"
|
iconClassName="mx_RoomTile_iconBell"
|
||||||
onClick={this.onClickAllNotifs}
|
onClick={this.onClickAllNotifs}
|
||||||
|
|
|
@ -1,917 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2016 OpenMarket Ltd
|
|
||||||
Copyright 2020 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 * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
|
||||||
import Modal from '../../../Modal';
|
|
||||||
import {
|
|
||||||
NotificationUtils,
|
|
||||||
VectorPushRulesDefinitions,
|
|
||||||
PushRuleVectorState,
|
|
||||||
ContentRules,
|
|
||||||
} from '../../../notifications';
|
|
||||||
import SdkConfig from "../../../SdkConfig";
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
// TODO: this "view" component still has far too much application logic in it,
|
|
||||||
// which should be factored out to other files.
|
|
||||||
|
|
||||||
// TODO: this component also does a lot of direct poking into this.state, which
|
|
||||||
// is VERY NAUGHTY.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rules that Vector used to set in order to override the actions of default rules.
|
|
||||||
* These are used to port peoples existing overrides to match the current API.
|
|
||||||
* These can be removed and forgotten once everyone has moved to the new client.
|
|
||||||
*/
|
|
||||||
const LEGACY_RULES = {
|
|
||||||
"im.vector.rule.contains_display_name": ".m.rule.contains_display_name",
|
|
||||||
"im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one",
|
|
||||||
"im.vector.rule.room_message": ".m.rule.message",
|
|
||||||
"im.vector.rule.invite_for_me": ".m.rule.invite_for_me",
|
|
||||||
"im.vector.rule.call": ".m.rule.call",
|
|
||||||
"im.vector.rule.notices": ".m.rule.suppress_notices",
|
|
||||||
};
|
|
||||||
|
|
||||||
function portLegacyActions(actions) {
|
|
||||||
const decoded = NotificationUtils.decodeActions(actions);
|
|
||||||
if (decoded !== null) {
|
|
||||||
return NotificationUtils.encodeActions(decoded);
|
|
||||||
} else {
|
|
||||||
// We don't recognise one of the actions here, so we don't try to
|
|
||||||
// canonicalise them.
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.settings.Notifications")
|
|
||||||
export default class Notifications extends React.Component {
|
|
||||||
static phases = {
|
|
||||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
|
||||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
|
||||||
ERROR: "ERROR", // There was an error
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
|
||||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
|
||||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
|
||||||
vectorState: PushRuleVectorState.ON,
|
|
||||||
rules: [],
|
|
||||||
},
|
|
||||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
|
||||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
|
||||||
threepids: [], // used for email notifications
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._refreshFromServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnableNotificationsChange = (checked) => {
|
|
||||||
const self = this;
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
MatrixClientPeg.get().setPushRuleEnabled(
|
|
||||||
'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked,
|
|
||||||
).then(function() {
|
|
||||||
self._refreshFromServer();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEnableDesktopNotificationsChange = (checked) => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"notificationsEnabled", null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
checked,
|
|
||||||
).finally(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEnableDesktopNotificationBodyChange = (checked) => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"notificationBodyEnabled", null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
checked,
|
|
||||||
).finally(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onEnableAudioNotificationsChange = (checked) => {
|
|
||||||
SettingsStore.setValue(
|
|
||||||
"audioNotificationsEnabled", null,
|
|
||||||
SettingLevel.DEVICE,
|
|
||||||
checked,
|
|
||||||
).finally(() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Returns the email pusher (pusher of type 'email') for a given
|
|
||||||
* email address. Email pushers all have the same app ID, so since
|
|
||||||
* pushers are unique over (app ID, pushkey), there will be at most
|
|
||||||
* one such pusher.
|
|
||||||
*/
|
|
||||||
getEmailPusher(pushers, address) {
|
|
||||||
if (pushers === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < pushers.length; ++i) {
|
|
||||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
|
||||||
return pushers[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnableEmailNotificationsChange = (address, checked) => {
|
|
||||||
let emailPusherPromise;
|
|
||||||
if (checked) {
|
|
||||||
const data = {};
|
|
||||||
data['brand'] = SdkConfig.get().brand;
|
|
||||||
emailPusherPromise = MatrixClientPeg.get().setPusher({
|
|
||||||
kind: 'email',
|
|
||||||
app_id: 'm.email',
|
|
||||||
pushkey: address,
|
|
||||||
app_display_name: 'Email Notifications',
|
|
||||||
device_display_name: address,
|
|
||||||
lang: navigator.language,
|
|
||||||
data: data,
|
|
||||||
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const emailPusher = this.getEmailPusher(this.state.pushers, address);
|
|
||||||
emailPusher.kind = null;
|
|
||||||
emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher);
|
|
||||||
}
|
|
||||||
emailPusherPromise.then(() => {
|
|
||||||
this._refreshFromServer();
|
|
||||||
}, (error) => {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, {
|
|
||||||
title: _t('Error saving email notification preferences'),
|
|
||||||
description: _t('An error occurred whilst saving your email notification preferences.'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onNotifStateButtonClicked = (event) => {
|
|
||||||
// FIXME: use .bind() rather than className metadata here surely
|
|
||||||
const vectorRuleId = event.target.className.split("-")[0];
|
|
||||||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
|
||||||
|
|
||||||
if ("_keywords" === vectorRuleId) {
|
|
||||||
this._setKeywordsPushRuleVectorState(newPushRuleVectorState);
|
|
||||||
} else {
|
|
||||||
const rule = this.getRule(vectorRuleId);
|
|
||||||
if (rule) {
|
|
||||||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onKeywordsClicked = (event) => {
|
|
||||||
// Compute the keywords list to display
|
|
||||||
let keywords = [];
|
|
||||||
for (const i in this.state.vectorContentRules.rules) {
|
|
||||||
const rule = this.state.vectorContentRules.rules[i];
|
|
||||||
keywords.push(rule.pattern);
|
|
||||||
}
|
|
||||||
if (keywords.length) {
|
|
||||||
// As keeping the order of per-word push rules hs side is a bit tricky to code,
|
|
||||||
// display the keywords in alphabetical order to the user
|
|
||||||
keywords.sort();
|
|
||||||
|
|
||||||
keywords = keywords.join(", ");
|
|
||||||
} else {
|
|
||||||
keywords = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
|
||||||
Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, {
|
|
||||||
title: _t('Keywords'),
|
|
||||||
description: _t('Enter keywords separated by a comma:'),
|
|
||||||
button: _t('OK'),
|
|
||||||
value: keywords,
|
|
||||||
onFinished: (shouldLeave, newValue) => {
|
|
||||||
if (shouldLeave && newValue !== keywords) {
|
|
||||||
let newKeywords = newValue.split(',');
|
|
||||||
for (const i in newKeywords) {
|
|
||||||
newKeywords[i] = newKeywords[i].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicates and empty
|
|
||||||
newKeywords = newKeywords.reduce(function(array, keyword) {
|
|
||||||
if (keyword !== "" && array.indexOf(keyword) < 0) {
|
|
||||||
array.push(keyword);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
this._setKeywords(newKeywords);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getRule(vectorRuleId) {
|
|
||||||
for (const i in this.state.vectorPushRules) {
|
|
||||||
const rule = this.state.vectorPushRules[i];
|
|
||||||
if (rule.vectorRuleId === vectorRuleId) {
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setPushRuleVectorState(rule, newPushRuleVectorState) {
|
|
||||||
if (rule && rule.vectorState !== newPushRuleVectorState) {
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const deferreds = [];
|
|
||||||
const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId];
|
|
||||||
|
|
||||||
if (rule.rule) {
|
|
||||||
const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState];
|
|
||||||
|
|
||||||
if (!actions) {
|
|
||||||
// The new state corresponds to disabling the rule.
|
|
||||||
deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false));
|
|
||||||
} else {
|
|
||||||
// The new state corresponds to enabling the rule and setting specific actions
|
|
||||||
deferreds.push(this._updatePushRuleActions(rule.rule, actions, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(deferreds).then(function() {
|
|
||||||
self._refreshFromServer();
|
|
||||||
}, function(error) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to change settings: " + error);
|
|
||||||
Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to change settings'),
|
|
||||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
|
||||||
onFinished: self._refreshFromServer,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setKeywordsPushRuleVectorState(newPushRuleVectorState) {
|
|
||||||
// Is there really a change?
|
|
||||||
if (this.state.vectorContentRules.vectorState === newPushRuleVectorState
|
|
||||||
|| this.state.vectorContentRules.rules.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update all rules in self.state.vectorContentRules
|
|
||||||
const deferreds = [];
|
|
||||||
for (const i in this.state.vectorContentRules.rules) {
|
|
||||||
const rule = this.state.vectorContentRules.rules[i];
|
|
||||||
|
|
||||||
let enabled; let actions;
|
|
||||||
switch (newPushRuleVectorState) {
|
|
||||||
case PushRuleVectorState.ON:
|
|
||||||
if (rule.actions.length !== 1) {
|
|
||||||
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PushRuleVectorState.LOUD:
|
|
||||||
if (rule.actions.length !== 3) {
|
|
||||||
actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) {
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PushRuleVectorState.OFF:
|
|
||||||
enabled = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actions) {
|
|
||||||
// Note that the workaround in _updatePushRuleActions will automatically
|
|
||||||
// enable the rule
|
|
||||||
deferreds.push(this._updatePushRuleActions(rule, actions, enabled));
|
|
||||||
} else if (enabled != undefined) {
|
|
||||||
deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(deferreds).then(function(resps) {
|
|
||||||
self._refreshFromServer();
|
|
||||||
}, function(error) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Can't update user notification settings: " + error);
|
|
||||||
Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, {
|
|
||||||
title: _t('Can\'t update user notification settings'),
|
|
||||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
|
||||||
onFinished: self._refreshFromServer,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setKeywords(newKeywords) {
|
|
||||||
this.setState({
|
|
||||||
phase: Notifications.phases.LOADING,
|
|
||||||
});
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const removeDeferreds = [];
|
|
||||||
|
|
||||||
// Remove per-word push rules of keywords that are no more in the list
|
|
||||||
const vectorContentRulesPatterns = [];
|
|
||||||
for (const i in self.state.vectorContentRules.rules) {
|
|
||||||
const rule = self.state.vectorContentRules.rules[i];
|
|
||||||
|
|
||||||
vectorContentRulesPatterns.push(rule.pattern);
|
|
||||||
|
|
||||||
if (newKeywords.indexOf(rule.pattern) < 0) {
|
|
||||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the keyword is part of `externalContentRules`, remove the rule
|
|
||||||
// before recreating it in the right Vector path
|
|
||||||
for (const i in self.state.externalContentRules) {
|
|
||||||
const rule = self.state.externalContentRules[i];
|
|
||||||
|
|
||||||
if (newKeywords.indexOf(rule.pattern) >= 0) {
|
|
||||||
removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onError = function(error) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to update keywords: " + error);
|
|
||||||
Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to update keywords'),
|
|
||||||
description: ((error && error.message) ? error.message : _t('Operation failed')),
|
|
||||||
onFinished: self._refreshFromServer,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Then, add the new ones
|
|
||||||
Promise.all(removeDeferreds).then(function(resps) {
|
|
||||||
const deferreds = [];
|
|
||||||
|
|
||||||
let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
|
|
||||||
if (pushRuleVectorStateKind === PushRuleVectorState.OFF) {
|
|
||||||
// When the current global keywords rule is OFF, we need to look at
|
|
||||||
// the flavor of rules in 'vectorContentRules' to apply the same actions
|
|
||||||
// when creating the new rule.
|
|
||||||
// Thus, this new rule will join the 'vectorContentRules' set.
|
|
||||||
if (self.state.vectorContentRules.rules.length) {
|
|
||||||
pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind(
|
|
||||||
self.state.vectorContentRules.rules[0],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// ON is default
|
|
||||||
pushRuleVectorStateKind = PushRuleVectorState.ON;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const i in newKeywords) {
|
|
||||||
const keyword = newKeywords[i];
|
|
||||||
|
|
||||||
if (vectorContentRulesPatterns.indexOf(keyword) < 0) {
|
|
||||||
if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) {
|
|
||||||
deferreds.push(cli.addPushRule('global', 'content', keyword, {
|
|
||||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
|
||||||
pattern: keyword,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
deferreds.push(self._addDisabledPushRule('global', 'content', keyword, {
|
|
||||||
actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind),
|
|
||||||
pattern: keyword,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(deferreds).then(function(resps) {
|
|
||||||
self._refreshFromServer();
|
|
||||||
}, onError);
|
|
||||||
}, onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a push rule but disabled
|
|
||||||
_addDisabledPushRule(scope, kind, ruleId, body) {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
|
||||||
cli.setPushRuleEnabled(scope, kind, ruleId, false),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any legacy im.vector rules need to be ported to the new API
|
|
||||||
// for overriding the actions of default rules.
|
|
||||||
_portRulesToNewAPI(rulesets) {
|
|
||||||
const needsUpdate = [];
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
for (const kind in rulesets.global) {
|
|
||||||
const ruleset = rulesets.global[kind];
|
|
||||||
for (let i = 0; i < ruleset.length; ++i) {
|
|
||||||
const rule = ruleset[i];
|
|
||||||
if (rule.rule_id in LEGACY_RULES) {
|
|
||||||
console.log("Porting legacy rule", rule);
|
|
||||||
needsUpdate.push( function(kind, rule) {
|
|
||||||
return cli.setPushRuleActions(
|
|
||||||
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions),
|
|
||||||
).then(() =>
|
|
||||||
cli.deletePushRule('global', kind, rule.rule_id),
|
|
||||||
).catch( (e) => {
|
|
||||||
console.warn(`Error when porting legacy rule: ${e}`);
|
|
||||||
});
|
|
||||||
}(kind, rule));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsUpdate.length > 0) {
|
|
||||||
// If some of the rules need to be ported then wait for the porting
|
|
||||||
// to happen and then fetch the rules again.
|
|
||||||
return Promise.all(needsUpdate).then(() =>
|
|
||||||
cli.getPushRules(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Otherwise return the rules that we already have.
|
|
||||||
return rulesets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshFromServer = () => {
|
|
||||||
const self = this;
|
|
||||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(
|
|
||||||
self._portRulesToNewAPI,
|
|
||||||
).then(function(rulesets) {
|
|
||||||
/// XXX seriously? wtf is this?
|
|
||||||
MatrixClientPeg.get().pushRules = rulesets;
|
|
||||||
|
|
||||||
// Get homeserver default rules and triage them by categories
|
|
||||||
const ruleCategories = {
|
|
||||||
// The master rule (all notifications disabling)
|
|
||||||
'.m.rule.master': 'master',
|
|
||||||
|
|
||||||
// The default push rules displayed by Vector UI
|
|
||||||
'.m.rule.contains_display_name': 'vector',
|
|
||||||
'.m.rule.contains_user_name': 'vector',
|
|
||||||
'.m.rule.roomnotif': 'vector',
|
|
||||||
'.m.rule.room_one_to_one': 'vector',
|
|
||||||
'.m.rule.encrypted_room_one_to_one': 'vector',
|
|
||||||
'.m.rule.message': 'vector',
|
|
||||||
'.m.rule.encrypted': 'vector',
|
|
||||||
'.m.rule.invite_for_me': 'vector',
|
|
||||||
//'.m.rule.member_event': 'vector',
|
|
||||||
'.m.rule.call': 'vector',
|
|
||||||
'.m.rule.suppress_notices': 'vector',
|
|
||||||
'.m.rule.tombstone': 'vector',
|
|
||||||
|
|
||||||
// Others go to others
|
|
||||||
};
|
|
||||||
|
|
||||||
// HS default rules
|
|
||||||
const defaultRules = { master: [], vector: {}, others: [] };
|
|
||||||
|
|
||||||
for (const kind in rulesets.global) {
|
|
||||||
for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) {
|
|
||||||
const r = rulesets.global[kind][i];
|
|
||||||
const cat = ruleCategories[r.rule_id];
|
|
||||||
r.kind = kind;
|
|
||||||
|
|
||||||
if (r.rule_id[0] === '.') {
|
|
||||||
if (cat === 'vector') {
|
|
||||||
defaultRules.vector[r.rule_id] = r;
|
|
||||||
} else if (cat === 'master') {
|
|
||||||
defaultRules.master.push(r);
|
|
||||||
} else {
|
|
||||||
defaultRules['others'].push(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the master rule if any defined by the hs
|
|
||||||
if (defaultRules.master.length > 0) {
|
|
||||||
self.state.masterPushRule = defaultRules.master[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the keyword rules into our state
|
|
||||||
const contentRules = ContentRules.parseContentRules(rulesets);
|
|
||||||
self.state.vectorContentRules = {
|
|
||||||
vectorState: contentRules.vectorState,
|
|
||||||
rules: contentRules.rules,
|
|
||||||
};
|
|
||||||
self.state.externalContentRules = contentRules.externalRules;
|
|
||||||
|
|
||||||
// Build the rules displayed in the Vector UI matrix table
|
|
||||||
self.state.vectorPushRules = [];
|
|
||||||
self.state.externalPushRules = [];
|
|
||||||
|
|
||||||
const vectorRuleIds = [
|
|
||||||
'.m.rule.contains_display_name',
|
|
||||||
'.m.rule.contains_user_name',
|
|
||||||
'.m.rule.roomnotif',
|
|
||||||
'_keywords',
|
|
||||||
'.m.rule.room_one_to_one',
|
|
||||||
'.m.rule.encrypted_room_one_to_one',
|
|
||||||
'.m.rule.message',
|
|
||||||
'.m.rule.encrypted',
|
|
||||||
'.m.rule.invite_for_me',
|
|
||||||
//'im.vector.rule.member_event',
|
|
||||||
'.m.rule.call',
|
|
||||||
'.m.rule.suppress_notices',
|
|
||||||
'.m.rule.tombstone',
|
|
||||||
];
|
|
||||||
for (const i in vectorRuleIds) {
|
|
||||||
const vectorRuleId = vectorRuleIds[i];
|
|
||||||
|
|
||||||
if (vectorRuleId === '_keywords') {
|
|
||||||
// keywords needs a special handling
|
|
||||||
// For Vector UI, this is a single global push rule but translated in Matrix,
|
|
||||||
// it corresponds to all content push rules (stored in self.state.vectorContentRule)
|
|
||||||
self.state.vectorPushRules.push({
|
|
||||||
"vectorRuleId": "_keywords",
|
|
||||||
"description": (
|
|
||||||
<span>
|
|
||||||
{ _t('Messages containing <span>keywords</span>',
|
|
||||||
{},
|
|
||||||
{ 'span': (sub) =>
|
|
||||||
<span className="mx_UserNotifSettings_keywords" onClick={ self.onKeywordsClicked }>{sub}</span>,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
"vectorState": self.state.vectorContentRules.vectorState,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId];
|
|
||||||
const rule = defaultRules.vector[vectorRuleId];
|
|
||||||
|
|
||||||
const vectorState = ruleDefinition.ruleToVectorState(rule);
|
|
||||||
|
|
||||||
//console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState);
|
|
||||||
|
|
||||||
self.state.vectorPushRules.push({
|
|
||||||
"vectorRuleId": vectorRuleId,
|
|
||||||
"description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js
|
|
||||||
"rule": rule,
|
|
||||||
"vectorState": vectorState,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if there was a rule which we couldn't parse, add it to the external list
|
|
||||||
if (rule && !vectorState) {
|
|
||||||
rule.description = ruleDefinition.description;
|
|
||||||
self.state.externalPushRules.push(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the rules not managed by Vector UI
|
|
||||||
const otherRulesDescriptions = {
|
|
||||||
'.m.rule.message': _t('Notify for all other messages/rooms'),
|
|
||||||
'.m.rule.fallback': _t('Notify me for anything else'),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const i in defaultRules.others) {
|
|
||||||
const rule = defaultRules.others[i];
|
|
||||||
const ruleDescription = otherRulesDescriptions[rule.rule_id];
|
|
||||||
|
|
||||||
// Show enabled default rules that was modified by the user
|
|
||||||
if (ruleDescription && rule.enabled && !rule.default) {
|
|
||||||
rule.description = ruleDescription;
|
|
||||||
self.state.externalPushRules.push(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) {
|
|
||||||
self.setState({ pushers: resp.pushers });
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
|
||||||
self.setState({
|
|
||||||
phase: Notifications.phases.DISPLAY,
|
|
||||||
});
|
|
||||||
}, function(error) {
|
|
||||||
console.error(error);
|
|
||||||
self.setState({
|
|
||||||
phase: Notifications.phases.ERROR,
|
|
||||||
});
|
|
||||||
}).finally(() => {
|
|
||||||
// actually explicitly update our state having been deep-manipulating it
|
|
||||||
self.setState({
|
|
||||||
masterPushRule: self.state.masterPushRule,
|
|
||||||
vectorContentRules: self.state.vectorContentRules,
|
|
||||||
vectorPushRules: self.state.vectorPushRules,
|
|
||||||
externalContentRules: self.state.externalContentRules,
|
|
||||||
externalPushRules: self.state.externalPushRules,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
MatrixClientPeg.get().getThreePids().then((r) => this.setState({ threepids: r.threepids }));
|
|
||||||
};
|
|
||||||
|
|
||||||
_onClearNotifications = () => {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
cli.getRooms().forEach(r => {
|
|
||||||
if (r.getUnreadNotificationCount() > 0) {
|
|
||||||
const events = r.getLiveTimeline().getEvents();
|
|
||||||
if (events.length) cli.sendReadReceipt(events.pop());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_updatePushRuleActions(rule, actions, enabled) {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
return cli.setPushRuleActions(
|
|
||||||
'global', rule.kind, rule.rule_id, actions,
|
|
||||||
).then( function() {
|
|
||||||
// Then, if requested, enabled or disabled the rule
|
|
||||||
if (undefined != enabled) {
|
|
||||||
return cli.setPushRuleEnabled(
|
|
||||||
'global', rule.kind, rule.rule_id, enabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotifRulesTableRow(title, className, pushRuleVectorState) {
|
|
||||||
return (
|
|
||||||
<tr key={ className }>
|
|
||||||
<th>
|
|
||||||
{ title }
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th>
|
|
||||||
<input className= {className + "-" + PushRuleVectorState.OFF}
|
|
||||||
type="radio"
|
|
||||||
checked={ pushRuleVectorState === PushRuleVectorState.OFF }
|
|
||||||
onChange={ this.onNotifStateButtonClicked } />
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th>
|
|
||||||
<input className= {className + "-" + PushRuleVectorState.ON}
|
|
||||||
type="radio"
|
|
||||||
checked={ pushRuleVectorState === PushRuleVectorState.ON }
|
|
||||||
onChange={ this.onNotifStateButtonClicked } />
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th>
|
|
||||||
<input className= {className + "-" + PushRuleVectorState.LOUD}
|
|
||||||
type="radio"
|
|
||||||
checked={ pushRuleVectorState === PushRuleVectorState.LOUD }
|
|
||||||
onChange={ this.onNotifStateButtonClicked } />
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotifRulesTableRows() {
|
|
||||||
const rows = [];
|
|
||||||
for (const i in this.state.vectorPushRules) {
|
|
||||||
const rule = this.state.vectorPushRules[i];
|
|
||||||
if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) {
|
|
||||||
console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
|
||||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEmailPusher(pushers, address) {
|
|
||||||
if (pushers === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < pushers.length; ++i) {
|
|
||||||
if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
emailNotificationsRow(address, label) {
|
|
||||||
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
|
|
||||||
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
|
|
||||||
label={label} key={`emailNotif_${label}`} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let spinner;
|
|
||||||
if (this.state.phase === Notifications.phases.LOADING) {
|
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
spinner = <Loader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let masterPushRuleDiv;
|
|
||||||
if (this.state.masterPushRule) {
|
|
||||||
masterPushRuleDiv = <LabelledToggleSwitch value={!this.state.masterPushRule.enabled}
|
|
||||||
onChange={this.onEnableNotificationsChange}
|
|
||||||
label={_t('Enable notifications for this account')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clearNotificationsButton;
|
|
||||||
if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
|
|
||||||
clearNotificationsButton = <AccessibleButton onClick={this._onClearNotifications} kind='danger'>
|
|
||||||
{_t("Clear notifications")}
|
|
||||||
</AccessibleButton>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When enabled, the master rule inhibits all existing rules
|
|
||||||
// So do not show all notification settings
|
|
||||||
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{masterPushRuleDiv}
|
|
||||||
|
|
||||||
<div className="mx_UserNotifSettings_notifTable">
|
|
||||||
{ _t('All notifications are currently disabled for all targets.') }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{clearNotificationsButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email");
|
|
||||||
let emailNotificationsRows;
|
|
||||||
if (emailThreepids.length > 0) {
|
|
||||||
emailNotificationsRows = emailThreepids.map((threePid) => this.emailNotificationsRow(
|
|
||||||
threePid.address, `${_t('Enable email notifications')} (${threePid.address})`,
|
|
||||||
));
|
|
||||||
} else if (SettingsStore.getValue(UIFeature.ThirdPartyID)) {
|
|
||||||
emailNotificationsRows = <div>
|
|
||||||
{ _t('Add an email address to configure email notifications') }
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build external push rules
|
|
||||||
const externalRules = [];
|
|
||||||
for (const i in this.state.externalPushRules) {
|
|
||||||
const rule = this.state.externalPushRules[i];
|
|
||||||
externalRules.push(<li>{ _t(rule.description) }</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show keywords not displayed by the vector UI as a single external push rule
|
|
||||||
let externalKeywords = [];
|
|
||||||
for (const i in this.state.externalContentRules) {
|
|
||||||
const rule = this.state.externalContentRules[i];
|
|
||||||
externalKeywords.push(rule.pattern);
|
|
||||||
}
|
|
||||||
if (externalKeywords.length) {
|
|
||||||
externalKeywords = externalKeywords.join(", ");
|
|
||||||
externalRules.push(<li>
|
|
||||||
{_t('Notifications on the following keywords follow rules which can’t be displayed here:') }
|
|
||||||
{ externalKeywords }
|
|
||||||
</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let devicesSection;
|
|
||||||
if (this.state.pushers === undefined) {
|
|
||||||
devicesSection = <div className="error">{ _t('Unable to fetch notification target list') }</div>;
|
|
||||||
} else if (this.state.pushers.length === 0) {
|
|
||||||
devicesSection = null;
|
|
||||||
} else {
|
|
||||||
// TODO: It would be great to be able to delete pushers from here too,
|
|
||||||
// and this wouldn't be hard to add.
|
|
||||||
const rows = [];
|
|
||||||
for (let i = 0; i < this.state.pushers.length; ++i) {
|
|
||||||
rows.push(<tr key={ i }>
|
|
||||||
<td>{this.state.pushers[i].app_display_name}</td>
|
|
||||||
<td>{this.state.pushers[i].device_display_name}</td>
|
|
||||||
</tr>);
|
|
||||||
}
|
|
||||||
devicesSection = (<table className="mx_UserNotifSettings_devicesTable">
|
|
||||||
<tbody>
|
|
||||||
{rows}
|
|
||||||
</tbody>
|
|
||||||
</table>);
|
|
||||||
}
|
|
||||||
if (devicesSection) {
|
|
||||||
devicesSection = (<div>
|
|
||||||
<h3>{ _t('Notification targets') }</h3>
|
|
||||||
{ devicesSection }
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let advancedSettings;
|
|
||||||
if (externalRules.length) {
|
|
||||||
const brand = SdkConfig.get().brand;
|
|
||||||
advancedSettings = (
|
|
||||||
<div>
|
|
||||||
<h3>{ _t('Advanced notification settings') }</h3>
|
|
||||||
{ _t('There are advanced notifications which are not shown here.') }<br />
|
|
||||||
{_t(
|
|
||||||
'You might have configured them in a client other than %(brand)s. ' +
|
|
||||||
'You cannot tune them in %(brand)s but they still apply.',
|
|
||||||
{ brand },
|
|
||||||
)}
|
|
||||||
<ul>
|
|
||||||
{ externalRules }
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
{masterPushRuleDiv}
|
|
||||||
|
|
||||||
<div className="mx_UserNotifSettings_notifTable">
|
|
||||||
|
|
||||||
{ spinner }
|
|
||||||
|
|
||||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
|
|
||||||
onChange={this.onEnableDesktopNotificationsChange}
|
|
||||||
label={_t('Enable desktop notifications for this session')} />
|
|
||||||
|
|
||||||
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
|
|
||||||
onChange={this.onEnableDesktopNotificationBodyChange}
|
|
||||||
label={_t('Show message in desktop notification')} />
|
|
||||||
|
|
||||||
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
|
|
||||||
onChange={this.onEnableAudioNotificationsChange}
|
|
||||||
label={_t('Enable audible notifications for this session')} />
|
|
||||||
|
|
||||||
{ emailNotificationsRows }
|
|
||||||
|
|
||||||
<div className="mx_UserNotifSettings_pushRulesTableWrapper">
|
|
||||||
<table className="mx_UserNotifSettings_pushRulesTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th width="55%"></th>
|
|
||||||
<th width="15%">{ _t('Off') }</th>
|
|
||||||
<th width="15%">{ _t('On') }</th>
|
|
||||||
<th width="15%">{ _t('Noisy') }</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
|
|
||||||
{ this.renderNotifRulesTableRows() }
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ advancedSettings }
|
|
||||||
|
|
||||||
{ devicesSection }
|
|
||||||
|
|
||||||
{ clearNotificationsButton }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
647
src/components/views/settings/Notifications.tsx
Normal file
647
src/components/views/settings/Notifications.tsx
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
/*
|
||||||
|
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.On) }</td>
|
||||||
|
<td>{ makeRadio(r, VectorState.Off) }</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("On") }</th>
|
||||||
|
<th>{ _t("Off") }</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>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,17 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import * as sdk from "../../../../../index";
|
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import Notifications from "../../Notifications";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
|
||||||
export default class NotificationUserSettingsTab extends React.Component {
|
export default class NotificationUserSettingsTab extends React.Component {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Notifications = sdk.getComponent("views.settings.Notifications");
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
|
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
|
|
@ -1133,33 +1133,24 @@
|
||||||
"Connecting to integration manager...": "Connecting to integration manager...",
|
"Connecting to integration manager...": "Connecting to integration manager...",
|
||||||
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
"Cannot connect to integration manager": "Cannot connect to integration manager",
|
||||||
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
|
||||||
"Error saving email notification preferences": "Error saving email notification preferences",
|
"Messages containing keywords": "Messages containing keywords",
|
||||||
"An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.",
|
"Error saving notification preferences": "Error saving notification preferences",
|
||||||
"Keywords": "Keywords",
|
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
||||||
"Enter keywords separated by a comma:": "Enter keywords separated by a comma:",
|
"Enable for this account": "Enable for this account",
|
||||||
"Failed to change settings": "Failed to change settings",
|
"Enable email notifications for %(email)s": "Enable email notifications for %(email)s",
|
||||||
"Can't update user notification settings": "Can't update user notification settings",
|
|
||||||
"Failed to update keywords": "Failed to update keywords",
|
|
||||||
"Messages containing <span>keywords</span>": "Messages containing <span>keywords</span>",
|
|
||||||
"Notify for all other messages/rooms": "Notify for all other messages/rooms",
|
|
||||||
"Notify me for anything else": "Notify me for anything else",
|
|
||||||
"Enable notifications for this account": "Enable notifications for this account",
|
|
||||||
"Clear notifications": "Clear notifications",
|
|
||||||
"All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.",
|
|
||||||
"Enable email notifications": "Enable email notifications",
|
|
||||||
"Add an email address to configure email notifications": "Add an email address to configure email notifications",
|
|
||||||
"Notifications on the following keywords follow rules which can’t be displayed here:": "Notifications on the following keywords follow rules which can’t be displayed here:",
|
|
||||||
"Unable to fetch notification target list": "Unable to fetch notification target list",
|
|
||||||
"Notification targets": "Notification targets",
|
|
||||||
"Advanced notification settings": "Advanced notification settings",
|
|
||||||
"There are advanced notifications which are not shown here.": "There are advanced notifications which are not shown here.",
|
|
||||||
"You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.",
|
|
||||||
"Enable desktop notifications for this session": "Enable desktop notifications for this session",
|
"Enable desktop notifications for this session": "Enable desktop notifications for this session",
|
||||||
"Show message in desktop notification": "Show message in desktop notification",
|
"Show message in desktop notification": "Show message in desktop notification",
|
||||||
"Enable audible notifications for this session": "Enable audible notifications for this session",
|
"Enable audible notifications for this session": "Enable audible notifications for this session",
|
||||||
"Off": "Off",
|
"Clear notifications": "Clear notifications",
|
||||||
|
"Keyword": "Keyword",
|
||||||
|
"New keyword": "New keyword",
|
||||||
|
"Global": "Global",
|
||||||
|
"Mentions & keywords": "Mentions & keywords",
|
||||||
"On": "On",
|
"On": "On",
|
||||||
|
"Off": "Off",
|
||||||
"Noisy": "Noisy",
|
"Noisy": "Noisy",
|
||||||
|
"Notification targets": "Notification targets",
|
||||||
|
"There was an error loading your notification settings.": "There was an error loading your notification settings.",
|
||||||
"Failed to save your profile": "Failed to save your profile",
|
"Failed to save your profile": "Failed to save your profile",
|
||||||
"The operation could not be completed": "The operation could not be completed",
|
"The operation could not be completed": "The operation could not be completed",
|
||||||
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
|
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
|
||||||
|
@ -1658,7 +1649,6 @@
|
||||||
"Show %(count)s more|other": "Show %(count)s more",
|
"Show %(count)s more|other": "Show %(count)s more",
|
||||||
"Show %(count)s more|one": "Show %(count)s more",
|
"Show %(count)s more|one": "Show %(count)s more",
|
||||||
"Show less": "Show less",
|
"Show less": "Show less",
|
||||||
"Use default": "Use default",
|
|
||||||
"All messages": "All messages",
|
"All messages": "All messages",
|
||||||
"Mentions & Keywords": "Mentions & Keywords",
|
"Mentions & Keywords": "Mentions & Keywords",
|
||||||
"Notification options": "Notification options",
|
"Notification options": "Notification options",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PushRuleVectorState, State } from "./PushRuleVectorState";
|
import { PushRuleVectorState, VectorState } from "./PushRuleVectorState";
|
||||||
import { IExtendedPushRule, IRuleSets } from "./types";
|
import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
export interface IContentRules {
|
export interface IContentRules {
|
||||||
vectorState: State;
|
vectorState: VectorState;
|
||||||
rules: IExtendedPushRule[];
|
rules: IAnnotatedPushRule[];
|
||||||
externalRules: IExtendedPushRule[];
|
externalRules: IAnnotatedPushRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SCOPE = "global";
|
export const SCOPE = "global";
|
||||||
|
@ -39,9 +38,9 @@ export class ContentRules {
|
||||||
* externalRules: a list of other keyword rules, with states other than
|
* externalRules: a list of other keyword rules, with states other than
|
||||||
* vectorState
|
* vectorState
|
||||||
*/
|
*/
|
||||||
static parseContentRules(rulesets: IRuleSets): IContentRules {
|
public static parseContentRules(rulesets: IPushRules): IContentRules {
|
||||||
// first categorise the keyword rules in terms of their actions
|
// first categorise the keyword rules in terms of their actions
|
||||||
const contentRules = this._categoriseContentRules(rulesets);
|
const contentRules = ContentRules.categoriseContentRules(rulesets);
|
||||||
|
|
||||||
// Decide which content rules to display in Vector UI.
|
// Decide which content rules to display in Vector UI.
|
||||||
// Vector displays a single global rule for a list of keywords
|
// Vector displays a single global rule for a list of keywords
|
||||||
|
@ -59,7 +58,7 @@ export class ContentRules {
|
||||||
|
|
||||||
if (contentRules.loud.length) {
|
if (contentRules.loud.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.Loud,
|
vectorState: VectorState.Loud,
|
||||||
rules: contentRules.loud,
|
rules: contentRules.loud,
|
||||||
externalRules: [
|
externalRules: [
|
||||||
...contentRules.loud_but_disabled,
|
...contentRules.loud_but_disabled,
|
||||||
|
@ -70,33 +69,33 @@ export class ContentRules {
|
||||||
};
|
};
|
||||||
} else if (contentRules.loud_but_disabled.length) {
|
} else if (contentRules.loud_but_disabled.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.Off,
|
vectorState: VectorState.Off,
|
||||||
rules: contentRules.loud_but_disabled,
|
rules: contentRules.loud_but_disabled,
|
||||||
externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
|
externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
|
||||||
};
|
};
|
||||||
} else if (contentRules.on.length) {
|
} else if (contentRules.on.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.On,
|
vectorState: VectorState.On,
|
||||||
rules: contentRules.on,
|
rules: contentRules.on,
|
||||||
externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
|
externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
|
||||||
};
|
};
|
||||||
} else if (contentRules.on_but_disabled.length) {
|
} else if (contentRules.on_but_disabled.length) {
|
||||||
return {
|
return {
|
||||||
vectorState: State.Off,
|
vectorState: VectorState.Off,
|
||||||
rules: contentRules.on_but_disabled,
|
rules: contentRules.on_but_disabled,
|
||||||
externalRules: contentRules.other,
|
externalRules: contentRules.other,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
vectorState: State.On,
|
vectorState: VectorState.On,
|
||||||
rules: [],
|
rules: [],
|
||||||
externalRules: contentRules.other,
|
externalRules: contentRules.other,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static _categoriseContentRules(rulesets: IRuleSets) {
|
private static categoriseContentRules(rulesets: IPushRules) {
|
||||||
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = {
|
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IAnnotatedPushRule[]> = {
|
||||||
on: [],
|
on: [],
|
||||||
on_but_disabled: [],
|
on_but_disabled: [],
|
||||||
loud: [],
|
loud: [],
|
||||||
|
@ -109,7 +108,7 @@ export class ContentRules {
|
||||||
const r = rulesets.global[kind][i];
|
const r = rulesets.global[kind][i];
|
||||||
|
|
||||||
// check it's not a default rule
|
// check it's not a default rule
|
||||||
if (r.rule_id[0] === '.' || kind !== "content") {
|
if (r.rule_id[0] === '.' || kind !== PushRuleKind.ContentSpecific) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,14 +116,14 @@ export class ContentRules {
|
||||||
r.kind = kind;
|
r.kind = kind;
|
||||||
|
|
||||||
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
|
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
|
||||||
case State.On:
|
case VectorState.On:
|
||||||
if (r.enabled) {
|
if (r.enabled) {
|
||||||
contentRules.on.push(r);
|
contentRules.on.push(r);
|
||||||
} else {
|
} else {
|
||||||
contentRules.on_but_disabled.push(r);
|
contentRules.on_but_disabled.push(r);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case State.Loud:
|
case VectorState.Loud:
|
||||||
if (r.enabled) {
|
if (r.enabled) {
|
||||||
contentRules.loud.push(r);
|
contentRules.loud.push(r);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Action, Actions } from "./types";
|
import { PushRuleAction, PushRuleActionName, TweakHighlight, TweakSound } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
interface IEncodedActions {
|
interface IEncodedActions {
|
||||||
notify: boolean;
|
notify: boolean;
|
||||||
|
@ -30,23 +29,23 @@ export class NotificationUtils {
|
||||||
// "highlight: true/false,
|
// "highlight: true/false,
|
||||||
// }
|
// }
|
||||||
// to a list of push actions.
|
// to a list of push actions.
|
||||||
static encodeActions(action: IEncodedActions) {
|
static encodeActions(action: IEncodedActions): PushRuleAction[] {
|
||||||
const notify = action.notify;
|
const notify = action.notify;
|
||||||
const sound = action.sound;
|
const sound = action.sound;
|
||||||
const highlight = action.highlight;
|
const highlight = action.highlight;
|
||||||
if (notify) {
|
if (notify) {
|
||||||
const actions: Action[] = [Actions.Notify];
|
const actions: PushRuleAction[] = [PushRuleActionName.Notify];
|
||||||
if (sound) {
|
if (sound) {
|
||||||
actions.push({ "set_tweak": "sound", "value": sound });
|
actions.push({ "set_tweak": "sound", "value": sound } as TweakSound);
|
||||||
}
|
}
|
||||||
if (highlight) {
|
if (highlight) {
|
||||||
actions.push({ "set_tweak": "highlight" });
|
actions.push({ "set_tweak": "highlight" } as TweakHighlight);
|
||||||
} else {
|
} else {
|
||||||
actions.push({ "set_tweak": "highlight", "value": false });
|
actions.push({ "set_tweak": "highlight", "value": false } as TweakHighlight);
|
||||||
}
|
}
|
||||||
return actions;
|
return actions;
|
||||||
} else {
|
} else {
|
||||||
return [Actions.DontNotify];
|
return [PushRuleActionName.DontNotify];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,16 +55,16 @@ export class NotificationUtils {
|
||||||
// "highlight: true/false,
|
// "highlight: true/false,
|
||||||
// }
|
// }
|
||||||
// If the actions couldn't be decoded then returns null.
|
// If the actions couldn't be decoded then returns null.
|
||||||
static decodeActions(actions: Action[]): IEncodedActions {
|
static decodeActions(actions: PushRuleAction[]): IEncodedActions {
|
||||||
let notify = false;
|
let notify = false;
|
||||||
let sound = null;
|
let sound = null;
|
||||||
let highlight = false;
|
let highlight = false;
|
||||||
|
|
||||||
for (let i = 0; i < actions.length; ++i) {
|
for (let i = 0; i < actions.length; ++i) {
|
||||||
const action = actions[i];
|
const action = actions[i];
|
||||||
if (action === Actions.Notify) {
|
if (action === PushRuleActionName.Notify) {
|
||||||
notify = true;
|
notify = true;
|
||||||
} else if (action === Actions.DontNotify) {
|
} else if (action === PushRuleActionName.DontNotify) {
|
||||||
notify = false;
|
notify = false;
|
||||||
} else if (typeof action === "object") {
|
} else if (typeof action === "object") {
|
||||||
if (action.set_tweak === "sound") {
|
if (action.set_tweak === "sound") {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,9 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import { StandardActions } from "./StandardActions";
|
import { StandardActions } from "./StandardActions";
|
||||||
import { NotificationUtils } from "./NotificationUtils";
|
import { NotificationUtils } from "./NotificationUtils";
|
||||||
import { IPushRule } from "./types";
|
import { IPushRule } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
export enum State {
|
export enum VectorState {
|
||||||
/** The push rule is disabled */
|
/** The push rule is disabled */
|
||||||
Off = "off",
|
Off = "off",
|
||||||
/** The user will receive push notification for this rule */
|
/** The user will receive push notification for this rule */
|
||||||
|
@ -31,26 +30,26 @@ export enum State {
|
||||||
|
|
||||||
export class PushRuleVectorState {
|
export class PushRuleVectorState {
|
||||||
// Backwards compatibility (things should probably be using the enum above instead)
|
// Backwards compatibility (things should probably be using the enum above instead)
|
||||||
static OFF = State.Off;
|
static OFF = VectorState.Off;
|
||||||
static ON = State.On;
|
static ON = VectorState.On;
|
||||||
static LOUD = State.Loud;
|
static LOUD = VectorState.Loud;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum for state of a push rule as defined by the Vector UI.
|
* Enum for state of a push rule as defined by the Vector UI.
|
||||||
* @readonly
|
* @readonly
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
static states = State;
|
static states = VectorState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a PushRuleVectorState to a list of actions
|
* Convert a PushRuleVectorState to a list of actions
|
||||||
*
|
*
|
||||||
* @return [object] list of push-rule actions
|
* @return [object] list of push-rule actions
|
||||||
*/
|
*/
|
||||||
static actionsFor(pushRuleVectorState: State) {
|
static actionsFor(pushRuleVectorState: VectorState) {
|
||||||
if (pushRuleVectorState === State.On) {
|
if (pushRuleVectorState === VectorState.On) {
|
||||||
return StandardActions.ACTION_NOTIFY;
|
return StandardActions.ACTION_NOTIFY;
|
||||||
} else if (pushRuleVectorState === State.Loud) {
|
} else if (pushRuleVectorState === VectorState.Loud) {
|
||||||
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
|
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +61,7 @@ export class PushRuleVectorState {
|
||||||
* category or in PushRuleVectorState.LOUD, regardless of its enabled
|
* category or in PushRuleVectorState.LOUD, regardless of its enabled
|
||||||
* state. Returns null if it does not match these categories.
|
* state. Returns null if it does not match these categories.
|
||||||
*/
|
*/
|
||||||
static contentRuleVectorStateKind(rule: IPushRule): State {
|
static contentRuleVectorStateKind(rule: IPushRule): VectorState {
|
||||||
const decoded = NotificationUtils.decodeActions(rule.actions);
|
const decoded = NotificationUtils.decodeActions(rule.actions);
|
||||||
|
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
|
@ -80,10 +79,10 @@ export class PushRuleVectorState {
|
||||||
let stateKind = null;
|
let stateKind = null;
|
||||||
switch (tweaks) {
|
switch (tweaks) {
|
||||||
case 0:
|
case 0:
|
||||||
stateKind = State.On;
|
stateKind = VectorState.On;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
stateKind = State.Loud;
|
stateKind = VectorState.Loud;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return stateKind;
|
return stateKind;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,19 +16,24 @@ limitations under the License.
|
||||||
|
|
||||||
import { _td } from '../languageHandler';
|
import { _td } from '../languageHandler';
|
||||||
import { StandardActions } from "./StandardActions";
|
import { StandardActions } from "./StandardActions";
|
||||||
import { PushRuleVectorState } from "./PushRuleVectorState";
|
import { PushRuleVectorState, VectorState } from "./PushRuleVectorState";
|
||||||
import { NotificationUtils } from "./NotificationUtils";
|
import { NotificationUtils } from "./NotificationUtils";
|
||||||
|
import { PushRuleAction, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||||
|
|
||||||
|
type StateToActionsMap = {
|
||||||
|
[state in VectorState]?: PushRuleAction[];
|
||||||
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
kind: Kind;
|
kind: PushRuleKind;
|
||||||
description: string;
|
description: string;
|
||||||
vectorStateToActions: Action;
|
vectorStateToActions: StateToActionsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VectorPushRuleDefinition {
|
class VectorPushRuleDefinition {
|
||||||
private kind: Kind;
|
private kind: PushRuleKind;
|
||||||
private description: string;
|
private description: string;
|
||||||
private vectorStateToActions: Action;
|
public readonly vectorStateToActions: StateToActionsMap;
|
||||||
|
|
||||||
constructor(opts: IProps) {
|
constructor(opts: IProps) {
|
||||||
this.kind = opts.kind;
|
this.kind = opts.kind;
|
||||||
|
@ -73,73 +77,62 @@ class VectorPushRuleDefinition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Kind {
|
|
||||||
Override = "override",
|
|
||||||
Underride = "underride",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Action {
|
|
||||||
on: StandardActions;
|
|
||||||
loud: StandardActions;
|
|
||||||
off: StandardActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The descriptions of rules managed by the Vector UI.
|
* The descriptions of rules managed by the Vector UI.
|
||||||
*/
|
*/
|
||||||
export const VectorPushRulesDefinitions = {
|
export const VectorPushRulesDefinitions = {
|
||||||
// Messages containing user's display name
|
// Messages containing user's display name
|
||||||
".m.rule.contains_display_name": new VectorPushRuleDefinition({
|
".m.rule.contains_display_name": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Messages containing user's username (localpart/MXID)
|
// Messages containing user's username (localpart/MXID)
|
||||||
".m.rule.contains_user_name": new VectorPushRuleDefinition({
|
".m.rule.contains_user_name": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Messages containing @room
|
// Messages containing @room
|
||||||
".m.rule.roomnotif": new VectorPushRuleDefinition({
|
".m.rule.roomnotif": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Messages just sent to the user in a 1:1 room
|
// Messages just sent to the user in a 1:1 room
|
||||||
".m.rule.room_one_to_one": new VectorPushRuleDefinition({
|
".m.rule.room_one_to_one": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Encrypted messages just sent to the user in a 1:1 room
|
// Encrypted messages just sent to the user in a 1:1 room
|
||||||
".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({
|
".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -147,12 +140,12 @@ export const VectorPushRulesDefinitions = {
|
||||||
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
|
// 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
|
||||||
// By opposition, all other room messages are from group chat rooms.
|
// By opposition, all other room messages are from group chat rooms.
|
||||||
".m.rule.message": new VectorPushRuleDefinition({
|
".m.rule.message": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -160,57 +153,57 @@ export const VectorPushRulesDefinitions = {
|
||||||
// Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
|
// Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
|
||||||
// By opposition, all other room messages are from group chat rooms.
|
// By opposition, all other room messages are from group chat rooms.
|
||||||
".m.rule.encrypted": new VectorPushRuleDefinition({
|
".m.rule.encrypted": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Invitation for the user
|
// Invitation for the user
|
||||||
".m.rule.invite_for_me": new VectorPushRuleDefinition({
|
".m.rule.invite_for_me": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Incoming call
|
// Incoming call
|
||||||
".m.rule.call": new VectorPushRuleDefinition({
|
".m.rule.call": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Underride,
|
kind: PushRuleKind.Underride,
|
||||||
description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_NOTIFY_RING_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_RING_SOUND,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Notifications from bots
|
// Notifications from bots
|
||||||
".m.rule.suppress_notices": new VectorPushRuleDefinition({
|
".m.rule.suppress_notices": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: {
|
vectorStateToActions: {
|
||||||
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
|
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
|
||||||
on: StandardActions.ACTION_DISABLED,
|
[VectorState.On]: StandardActions.ACTION_DISABLED,
|
||||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Room upgrades (tombstones)
|
// Room upgrades (tombstones)
|
||||||
".m.rule.tombstone": new VectorPushRuleDefinition({
|
".m.rule.tombstone": new VectorPushRuleDefinition({
|
||||||
kind: Kind.Override,
|
kind: PushRuleKind.Override,
|
||||||
description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js
|
||||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||||
on: StandardActions.ACTION_NOTIFY,
|
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||||
loud: StandardActions.ACTION_HIGHLIGHT,
|
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT,
|
||||||
off: StandardActions.ACTION_DISABLED,
|
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export enum NotificationSetting {
|
|
||||||
AllMessages = "all_messages", // .m.rule.message = notify
|
|
||||||
DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default.
|
|
||||||
MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread
|
|
||||||
Never = "never", // .m.rule.master = enabled (dont_notify)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISoundTweak {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
set_tweak: "sound";
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
export interface IHighlightTweak {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
set_tweak: "highlight";
|
|
||||||
value?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Tweak = ISoundTweak | IHighlightTweak;
|
|
||||||
|
|
||||||
export enum Actions {
|
|
||||||
Notify = "notify",
|
|
||||||
DontNotify = "dont_notify", // no-op
|
|
||||||
Coalesce = "coalesce", // unused
|
|
||||||
MarkUnread = "mark_unread", // new
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Action = Actions | Tweak;
|
|
||||||
|
|
||||||
// Push rule kinds in descending priority order
|
|
||||||
export enum Kind {
|
|
||||||
Override = "override",
|
|
||||||
ContentSpecific = "content",
|
|
||||||
RoomSpecific = "room",
|
|
||||||
SenderSpecific = "sender",
|
|
||||||
Underride = "underride",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEventMatchCondition {
|
|
||||||
kind: "event_match";
|
|
||||||
key: string;
|
|
||||||
pattern: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IContainsDisplayNameCondition {
|
|
||||||
kind: "contains_display_name";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRoomMemberCountCondition {
|
|
||||||
kind: "room_member_count";
|
|
||||||
is: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISenderNotificationPermissionCondition {
|
|
||||||
kind: "sender_notification_permission";
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Condition =
|
|
||||||
IEventMatchCondition |
|
|
||||||
IContainsDisplayNameCondition |
|
|
||||||
IRoomMemberCountCondition |
|
|
||||||
ISenderNotificationPermissionCondition;
|
|
||||||
|
|
||||||
export enum RuleIds {
|
|
||||||
MasterRule = ".m.rule.master", // The master rule (all notifications disabling)
|
|
||||||
MessageRule = ".m.rule.message",
|
|
||||||
EncryptedMessageRule = ".m.rule.encrypted",
|
|
||||||
RoomOneToOneRule = ".m.rule.room_one_to_one",
|
|
||||||
EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPushRule {
|
|
||||||
enabled: boolean;
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
rule_id: RuleIds | string;
|
|
||||||
actions: Action[];
|
|
||||||
default: boolean;
|
|
||||||
conditions?: Condition[]; // only applicable to `underride` and `override` rules
|
|
||||||
pattern?: string; // only applicable to `content` rules
|
|
||||||
}
|
|
||||||
|
|
||||||
// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor
|
|
||||||
export interface IExtendedPushRule extends IPushRule {
|
|
||||||
kind: Kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPushRuleSet {
|
|
||||||
override: IPushRule[];
|
|
||||||
content: IPushRule[];
|
|
||||||
room: IPushRule[];
|
|
||||||
sender: IPushRule[];
|
|
||||||
underride: IPushRule[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRuleSets {
|
|
||||||
global: IPushRuleSet;
|
|
||||||
}
|
|
Loading…
Reference in a new issue