mirror of
https://github.com/element-hq/element-web
synced 2024-11-21 16:55:34 +03:00
GYU: Account Notification Settings (#11008)
* Implement new notification settings UI * Sort new keywords at the front * Make ts-strict happier * Make ts-strict happier * chore: fixed lint issues * update beta card * Fix issue with the user settings test * chore: fixed lint issues * Add tests for notification settings * chore: fixed lint issues * fix: spurious text failures * improve tests further * make ts-strict happier * improve tests further * Reduce uncovered conditions * Correct snapshot * even more test coverage * Fix an issue with inverted rules * Update res/css/views/settings/tabs/_SettingsIndent.pcss Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com> * Fix license headers * Improve i18n * make linters happier * Improve beta labels * improve i18n * chore: fixed lint issues * fix: more lint issues * Update snapshots to match changed text * Update text as requested * Remove labs image * Update snapshots * Correct an issue with one of the tests * fix: keyword reconcilation code * Determine mute status more accurately * Address review comments * Prevent duplicate updates * Fix missing license header * slight change to avoid ts-strict complaining * fix test issue caused by previous merge --------- Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
parent
95283d21bb
commit
f62fe2626c
25 changed files with 3797 additions and 33 deletions
|
@ -321,6 +321,8 @@
|
|||
@import "./views/settings/_JoinRuleSettings.pcss";
|
||||
@import "./views/settings/_KeyboardShortcut.pcss";
|
||||
@import "./views/settings/_LayoutSwitcher.pcss";
|
||||
@import "./views/settings/_NotificationPusherSettings.pcss";
|
||||
@import "./views/settings/_NotificationSettings2.pcss";
|
||||
@import "./views/settings/_Notifications.pcss";
|
||||
@import "./views/settings/_PhoneNumbers.pcss";
|
||||
@import "./views/settings/_ProfileSettings.pcss";
|
||||
|
@ -331,6 +333,8 @@
|
|||
@import "./views/settings/_SpellCheckLanguages.pcss";
|
||||
@import "./views/settings/_ThemeChoicePanel.pcss";
|
||||
@import "./views/settings/_UpdateCheckButton.pcss";
|
||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||
@import "./views/settings/tabs/_SettingsTab.pcss";
|
||||
@import "./views/settings/tabs/room/_NotificationSettingsTab.pcss";
|
||||
|
|
26
res/css/views/settings/_NotificationPusherSettings.pcss
Normal file
26
res/css/views/settings/_NotificationPusherSettings.pcss
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2023 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_NotificationPusherSettings {
|
||||
.mx_NotificationPusherSettings_description {
|
||||
color: $primary-content;
|
||||
}
|
||||
|
||||
.mx_NotificationPusherSettings_detail {
|
||||
margin-top: -4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
85
res/css/views/settings/_NotificationSettings2.pcss
Normal file
85
res/css/views/settings/_NotificationSettings2.pcss
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2023 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_NotificationSettings2 {
|
||||
.mx_SettingsSection_subSections {
|
||||
color: $primary-content;
|
||||
gap: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mx_SettingsSubsection_description {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.mx_SettingsSubsection_text {
|
||||
font-size: 1.2rem;
|
||||
|
||||
.mx_NotificationBadge {
|
||||
vertical-align: baseline;
|
||||
display: inline-flex;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SettingsSubsection_content {
|
||||
margin-top: 12px;
|
||||
grid-gap: 12px;
|
||||
justify-items: stretch;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.mx_SettingsBanner {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.mx_NotificationSettings2_flags {
|
||||
grid-gap: 4px;
|
||||
}
|
||||
|
||||
.mx_StyledRadioButton_content {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mx_TagComposer {
|
||||
margin-top: 16px;
|
||||
|
||||
&.mx_TagComposer_disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mx_TagComposer_tags {
|
||||
margin-top: 16px;
|
||||
gap: 8px;
|
||||
|
||||
.mx_Tag {
|
||||
border-radius: 18px;
|
||||
line-height: 2.4rem;
|
||||
padding: 6px 12px;
|
||||
background: $panel-actions;
|
||||
margin: 0;
|
||||
|
||||
.mx_Tag_delete {
|
||||
background: $tertiary-content;
|
||||
color: #fff;
|
||||
align-self: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
res/css/views/settings/tabs/_SettingsBanner.pcss
Normal file
35
res/css/views/settings/tabs/_SettingsBanner.pcss
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2023 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_SettingsBanner {
|
||||
background: $system;
|
||||
line-height: 2.25rem;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.mx_SettingsBanner_content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
align-self: initial;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
22
res/css/views/settings/tabs/_SettingsIndent.pcss
Normal file
22
res/css/views/settings/tabs/_SettingsIndent.pcss
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
Copyright 2023 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_SettingsIndent {
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
4
res/img/element-icons/new-and-improved.svg
Normal file
4
res/img/element-icons/new-and-improved.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#0dbd8b" fill-opacity="0.1" d="m 24,12 a 12,12 0 0 1 -12,12 12,12 0 0 1 -12,-12 12,12 0 0 1 12,-12 12,12 0 0 1 12,12 z" />
|
||||
<path fill="#0dbd8b" d="m 20,12 a 8,8 0 0 1 -8,8 8,8 0 0 1 -8,-8 8,8 0 0 1 8,-8 8,8 0 0 1 8,8 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 334 B |
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-2023 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.
|
||||
|
@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import { Icon as CancelRounded } from "../../../../res/img/element-icons/cancel-rounded.svg";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
icon?: () => JSX.Element;
|
||||
label: string;
|
||||
onDeleteClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Tag: React.FC<IProps> = ({ icon, label, onDeleteClick, disabled = false }) => {
|
||||
export const Tag: React.FC<IProps> = ({ icon, label, onDeleteClick, disabled = false, ...other }) => {
|
||||
return (
|
||||
<div className="mx_Tag">
|
||||
<div className="mx_Tag" {...other}>
|
||||
{icon?.()}
|
||||
{label}
|
||||
{onDeleteClick && (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021-2023 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.
|
||||
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from "classnames";
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
|
||||
import Field from "./Field";
|
||||
|
@ -22,6 +23,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||
import { Tag } from "./Tag";
|
||||
|
||||
interface IProps {
|
||||
id?: string;
|
||||
tags: string[];
|
||||
onAdd: (tag: string) => void;
|
||||
onRemove: (tag: string) => void;
|
||||
|
@ -67,9 +69,14 @@ export default class TagComposer extends React.PureComponent<IProps, IState> {
|
|||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_TagComposer">
|
||||
<div
|
||||
className={classNames("mx_TagComposer", {
|
||||
mx_TagComposer_disabled: this.props.disabled,
|
||||
})}
|
||||
>
|
||||
<form className="mx_TagComposer_input" onSubmit={this.onAdd}>
|
||||
<Field
|
||||
id={this.props.id ? this.props.id + "_field" : undefined}
|
||||
value={this.state.newTag}
|
||||
onChange={this.onInputChange}
|
||||
label={this.props.label || _t("Keyword")}
|
||||
|
@ -81,13 +88,14 @@ export default class TagComposer extends React.PureComponent<IProps, IState> {
|
|||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
<div className="mx_TagComposer_tags">
|
||||
<div className="mx_TagComposer_tags" role="list">
|
||||
{this.props.tags.map((t, i) => (
|
||||
<Tag
|
||||
label={t}
|
||||
key={t}
|
||||
onDeleteClick={this.onRemove.bind(this, t)}
|
||||
disabled={this.props.disabled}
|
||||
role="listitem"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
Copyright 2023 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 { ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { IPusher } from "matrix-js-sdk/src/matrix";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import dispatcher from "../../../../dispatcher/dispatcher";
|
||||
import { usePushers } from "../../../../hooks/usePushers";
|
||||
import { useThreepids } from "../../../../hooks/useThreepids";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import SdkConfig from "../../../../SdkConfig";
|
||||
import { UserTab } from "../../dialogs/UserTab";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import LabelledCheckbox from "../../elements/LabelledCheckbox";
|
||||
import { SettingsIndent } from "../shared/SettingsIndent";
|
||||
import SettingsSubsection, { SettingsSubsectionText } from "../shared/SettingsSubsection";
|
||||
|
||||
function generalTabButton(content: string): JSX.Element {
|
||||
return (
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
onClick={() => {
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.General,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function NotificationPusherSettings(): JSX.Element {
|
||||
const EmailPusherTemplate: Omit<IPusher, "pushkey" | "device_display_name" | "append"> = useMemo(
|
||||
() => ({
|
||||
kind: "email",
|
||||
app_id: "m.email",
|
||||
app_display_name: _t("Email Notifications"),
|
||||
lang: navigator.language,
|
||||
data: {
|
||||
brand: SdkConfig.get().brand,
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const cli = useMatrixClientContext();
|
||||
const [pushers, refreshPushers] = usePushers(cli);
|
||||
const [threepids, refreshThreepids] = useThreepids(cli);
|
||||
|
||||
const setEmailEnabled = useCallback(
|
||||
(email: string, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
cli.setPusher({
|
||||
...EmailPusherTemplate,
|
||||
pushkey: email,
|
||||
device_display_name: email,
|
||||
// We always append for email pushers since we don't want to stop other
|
||||
// accounts notifying to the same email address
|
||||
append: true,
|
||||
}).catch((err) => console.error(err));
|
||||
} else {
|
||||
const pusher = pushers.find((p) => p.kind === "email" && p.pushkey === email);
|
||||
if (pusher) {
|
||||
cli.removePusher(pusher.pushkey, pusher.app_id).catch((err) => console.error(err));
|
||||
}
|
||||
}
|
||||
refreshThreepids();
|
||||
refreshPushers();
|
||||
},
|
||||
[EmailPusherTemplate, cli, pushers, refreshPushers, refreshThreepids],
|
||||
);
|
||||
|
||||
const notificationTargets = pushers.filter((it) => it.kind !== "email");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSubsection className="mx_NotificationPusherSettings" heading={_t("Email summary")}>
|
||||
<SettingsSubsectionText className="mx_NotificationPusherSettings_description">
|
||||
{_t("Receive an email summary of missed notifications")}
|
||||
</SettingsSubsectionText>
|
||||
<div className="mx_SettingsSubsection_description mx_NotificationPusherSettings_detail">
|
||||
<SettingsSubsectionText>
|
||||
{_t(
|
||||
"Select which emails you want to send summaries to. Manage your emails in <button>General</button>.",
|
||||
{},
|
||||
{ button: generalTabButton },
|
||||
)}
|
||||
</SettingsSubsectionText>
|
||||
</div>
|
||||
<SettingsIndent>
|
||||
{threepids
|
||||
.filter((t) => t.medium === ThreepidMedium.Email)
|
||||
.map((email) => (
|
||||
<LabelledCheckbox
|
||||
key={email.address}
|
||||
label={email.address}
|
||||
value={pushers.find((it) => it.pushkey === email.address) !== undefined}
|
||||
onChange={(value) => setEmailEnabled(email.address, value)}
|
||||
/>
|
||||
))}
|
||||
</SettingsIndent>
|
||||
</SettingsSubsection>
|
||||
{notificationTargets.length > 0 && (
|
||||
<SettingsSubsection heading={_t("Notification targets")}>
|
||||
<ul>
|
||||
{pushers
|
||||
.filter((it) => it.kind !== "email")
|
||||
.map((pusher) => (
|
||||
<li key={pusher.pushkey}>{pusher.device_display_name || pusher.app_display_name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</SettingsSubsection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
Copyright 2023 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, { useState } from "react";
|
||||
|
||||
import NewAndImprovedIcon from "../../../../../res/img/element-icons/new-and-improved.svg";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import { useNotificationSettings } from "../../../../hooks/useNotificationSettings";
|
||||
import { useSettingValue } from "../../../../hooks/useSettings";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import {
|
||||
DefaultNotificationSettings,
|
||||
NotificationSettings,
|
||||
} from "../../../../models/notificationsettings/NotificationSettings";
|
||||
import { RoomNotifState } from "../../../../RoomNotifs";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import { NotificationColor } from "../../../../stores/notifications/NotificationColor";
|
||||
import { clearAllNotifications } from "../../../../utils/notifications";
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
import LabelledCheckbox from "../../elements/LabelledCheckbox";
|
||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||
import StyledRadioGroup from "../../elements/StyledRadioGroup";
|
||||
import TagComposer from "../../elements/TagComposer";
|
||||
import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge";
|
||||
import { SettingsBanner } from "../shared/SettingsBanner";
|
||||
import { SettingsSection } from "../shared/SettingsSection";
|
||||
import SettingsSubsection from "../shared/SettingsSubsection";
|
||||
import { NotificationPusherSettings } from "./NotificationPusherSettings";
|
||||
|
||||
enum NotificationDefaultLevels {
|
||||
AllMessages = "all_messages",
|
||||
PeopleMentionsKeywords = "people_mentions_keywords",
|
||||
MentionsKeywords = "mentions_keywords",
|
||||
}
|
||||
|
||||
function toDefaultLevels(levels: NotificationSettings["defaultLevels"]): NotificationDefaultLevels {
|
||||
if (levels.room === RoomNotifState.AllMessages) {
|
||||
return NotificationDefaultLevels.AllMessages;
|
||||
} else if (levels.dm === RoomNotifState.AllMessages) {
|
||||
return NotificationDefaultLevels.PeopleMentionsKeywords;
|
||||
} else {
|
||||
return NotificationDefaultLevels.MentionsKeywords;
|
||||
}
|
||||
}
|
||||
|
||||
const NotificationOptions = [
|
||||
{
|
||||
value: NotificationDefaultLevels.AllMessages,
|
||||
label: _t("All messages"),
|
||||
},
|
||||
{
|
||||
value: NotificationDefaultLevels.PeopleMentionsKeywords,
|
||||
label: _t("People, Mentions and Keywords"),
|
||||
},
|
||||
{
|
||||
value: NotificationDefaultLevels.MentionsKeywords,
|
||||
label: _t("Mentions and Keywords only"),
|
||||
},
|
||||
];
|
||||
|
||||
function boldText(text: string): JSX.Element {
|
||||
return <strong>{text}</strong>;
|
||||
}
|
||||
|
||||
function useHasUnreadNotifications(): boolean {
|
||||
const cli = useMatrixClientContext();
|
||||
return cli.getRooms().some((room) => room.getUnreadNotificationCount() > 0);
|
||||
}
|
||||
|
||||
export default function NotificationSettings2(): JSX.Element {
|
||||
const cli = useMatrixClientContext();
|
||||
|
||||
const desktopNotifications = useSettingValue<boolean>("notificationsEnabled");
|
||||
const desktopShowBody = useSettingValue<boolean>("notificationBodyEnabled");
|
||||
const audioNotifications = useSettingValue<boolean>("audioNotificationsEnabled");
|
||||
|
||||
const { model, hasPendingChanges, reconcile } = useNotificationSettings(cli);
|
||||
|
||||
const disabled = model === null || hasPendingChanges;
|
||||
const settings = model ?? DefaultNotificationSettings;
|
||||
|
||||
const [updatingUnread, setUpdatingUnread] = useState<boolean>(false);
|
||||
const hasUnreadNotifications = useHasUnreadNotifications();
|
||||
|
||||
return (
|
||||
<div className="mx_NotificationSettings2">
|
||||
{hasPendingChanges && model !== null && (
|
||||
<SettingsBanner
|
||||
icon={<img src={NewAndImprovedIcon} alt="" width={12} />}
|
||||
action={_t("Switch now")}
|
||||
onAction={() => reconcile(model!)}
|
||||
>
|
||||
{_t(
|
||||
"<strong>Update:</strong> We have updated our notification settings. This won’t affect your previously selected settings.",
|
||||
{},
|
||||
{ strong: boldText },
|
||||
)}
|
||||
</SettingsBanner>
|
||||
)}
|
||||
<SettingsSection heading={_t("Notifications")}>
|
||||
<div className="mx_SettingsSubsection_content mx_NotificationSettings2_flags">
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Enable notifications for this account")}
|
||||
value={!settings.globalMute}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
globalMute: !value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Enable desktop notifications for this session")}
|
||||
value={desktopNotifications}
|
||||
onChange={(value) =>
|
||||
SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, value)
|
||||
}
|
||||
/>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Show message preview in desktop notification")}
|
||||
value={desktopShowBody}
|
||||
onChange={(value) =>
|
||||
SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, value)
|
||||
}
|
||||
/>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("Enable audible notifications for this session")}
|
||||
value={audioNotifications}
|
||||
onChange={(value) =>
|
||||
SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SettingsSubsection
|
||||
heading={_t("I want to be notified for (Default Setting)")}
|
||||
description={_t("This setting will be applied by default to all your rooms.")}
|
||||
>
|
||||
<StyledRadioGroup
|
||||
name="defaultNotificationLevel"
|
||||
value={toDefaultLevels(settings.defaultLevels)}
|
||||
disabled={disabled}
|
||||
definitions={NotificationOptions}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
defaultLevels: {
|
||||
...model!.defaultLevels,
|
||||
dm:
|
||||
value !== NotificationDefaultLevels.MentionsKeywords
|
||||
? RoomNotifState.AllMessages
|
||||
: RoomNotifState.MentionsOnly,
|
||||
room:
|
||||
value === NotificationDefaultLevels.AllMessages
|
||||
? RoomNotifState.AllMessages
|
||||
: RoomNotifState.MentionsOnly,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
<SettingsSubsection
|
||||
heading={_t("Play a sound for")}
|
||||
description={_t("Applied by default to all rooms on all devices.")}
|
||||
>
|
||||
<LabelledCheckbox
|
||||
label="People"
|
||||
value={settings.sound.people !== undefined}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
sound: {
|
||||
...model!.sound,
|
||||
people: value ? "default" : undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledCheckbox
|
||||
label={_t("Mentions and Keywords")}
|
||||
value={settings.sound.mentions !== undefined}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
sound: {
|
||||
...model!.sound,
|
||||
mentions: value ? "default" : undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledCheckbox
|
||||
label={_t("Audio and Video calls")}
|
||||
value={settings.sound.calls !== undefined}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
sound: {
|
||||
...model!.sound,
|
||||
calls: value ? "ring" : undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
<SettingsSubsection heading={_t("Other things we think you might be interested in:")}>
|
||||
<LabelledCheckbox
|
||||
label={_t("Invited to a room")}
|
||||
value={settings.activity.invite}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
activity: {
|
||||
...model!.activity,
|
||||
invite: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledCheckbox
|
||||
label={_t("New room activity, upgrades and status messages occur")}
|
||||
value={settings.activity.status_event}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
activity: {
|
||||
...model!.activity,
|
||||
status_event: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledCheckbox
|
||||
label={_t("Messages sent by bots")}
|
||||
value={settings.activity.bot_notices}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
activity: {
|
||||
...model!.activity,
|
||||
bot_notices: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
<SettingsSubsection
|
||||
heading={_t("Mentions and Keywords")}
|
||||
description={_t(
|
||||
"Show a badge <badge/> when keywords are used in a room.",
|
||||
{},
|
||||
{
|
||||
badge: <StatelessNotificationBadge symbol="1" count={1} color={NotificationColor.Grey} />,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<LabelledCheckbox
|
||||
label={_t("Notify when someone mentions using @room")}
|
||||
value={settings.mentions.room}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
mentions: {
|
||||
...model!.mentions,
|
||||
room: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledCheckbox
|
||||
label={_t("Notify when someone mentions using @displayname or %(mxid)s", {
|
||||
mxid: cli.getUserId()!,
|
||||
})}
|
||||
value={settings.mentions.user}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
mentions: {
|
||||
...model!.mentions,
|
||||
user: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<LabelledCheckbox
|
||||
label={_t("Notify when someone uses a keyword")}
|
||||
byline={_t("Enter keywords here, or use for spelling variations or nicknames")}
|
||||
value={settings.mentions.keywords}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
mentions: {
|
||||
...model!.mentions,
|
||||
keywords: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TagComposer
|
||||
id="mx_NotificationSettings2_Keywords"
|
||||
tags={model?.keywords ?? []}
|
||||
disabled={disabled}
|
||||
onAdd={(keyword) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
keywords: [keyword, ...model!.keywords],
|
||||
});
|
||||
}}
|
||||
onRemove={(keyword) => {
|
||||
reconcile({
|
||||
...model!,
|
||||
keywords: model!.keywords.filter((it) => it !== keyword),
|
||||
});
|
||||
}}
|
||||
label={_t("Keyword")}
|
||||
placeholder={_t("New keyword")}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
<NotificationPusherSettings />
|
||||
<SettingsSubsection heading={_t("Quick Actions")}>
|
||||
{hasUnreadNotifications && (
|
||||
<AccessibleButton
|
||||
kind="primary_outline"
|
||||
disabled={updatingUnread}
|
||||
onClick={async () => {
|
||||
setUpdatingUnread(true);
|
||||
await clearAllNotifications(cli);
|
||||
setUpdatingUnread(false);
|
||||
}}
|
||||
>
|
||||
{_t("Mark all messages as read")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
<AccessibleButton
|
||||
kind="danger_outline"
|
||||
disabled={model === null}
|
||||
onClick={() => {
|
||||
reconcile(DefaultNotificationSettings);
|
||||
}}
|
||||
>
|
||||
{_t("Reset to default settings")}
|
||||
</AccessibleButton>
|
||||
</SettingsSubsection>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
39
src/components/views/settings/shared/SettingsBanner.tsx
Normal file
39
src/components/views/settings/shared/SettingsBanner.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2023 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, { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
import AccessibleButton from "../../elements/AccessibleButton";
|
||||
|
||||
interface Props {
|
||||
icon?: ReactNode;
|
||||
action?: ReactNode;
|
||||
onAction?: () => void;
|
||||
}
|
||||
|
||||
export function SettingsBanner({ children, icon, action, onAction }: PropsWithChildren<Props>): JSX.Element {
|
||||
return (
|
||||
<div className="mx_SettingsBanner">
|
||||
{icon}
|
||||
<div className="mx_SettingsBanner_content">{children}</div>
|
||||
{action && (
|
||||
<AccessibleButton kind="primary_outline" onClick={onAction ?? null}>
|
||||
{action}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
27
src/components/views/settings/shared/SettingsIndent.tsx
Normal file
27
src/components/views/settings/shared/SettingsIndent.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2023 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, { HTMLAttributes } from "react";
|
||||
|
||||
export interface SettingsIndentProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsIndent: React.FC<SettingsIndentProps> = ({ children, ...rest }) => (
|
||||
<div {...rest} className="mx_SettingsIndent">
|
||||
{children}
|
||||
</div>
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-2023 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.
|
||||
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classnames from "classnames";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
|
||||
import Heading from "../../typography/Heading";
|
||||
|
@ -40,8 +41,8 @@ export interface SettingsSectionProps extends HTMLAttributes<HTMLDivElement> {
|
|||
* </SettingsTab>
|
||||
* ```
|
||||
*/
|
||||
export const SettingsSection: React.FC<SettingsSectionProps> = ({ heading, children, ...rest }) => (
|
||||
<div {...rest} className="mx_SettingsSection">
|
||||
export const SettingsSection: React.FC<SettingsSectionProps> = ({ className, heading, children, ...rest }) => (
|
||||
<div {...rest} className={classnames("mx_SettingsSection", className)}>
|
||||
{typeof heading === "string" ? <Heading size="2">{heading}</Heading> : <>{heading}</>}
|
||||
<div className="mx_SettingsSection_subSections">{children}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019-2023 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.
|
||||
|
@ -17,17 +17,26 @@ limitations under the License.
|
|||
import React from "react";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { Features } from "../../../../../settings/Settings";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import Notifications from "../../Notifications";
|
||||
import NotificationSettings2 from "../../notifications/NotificationSettings2";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
|
||||
export default class NotificationUserSettingsTab extends React.Component {
|
||||
public render(): React.ReactNode {
|
||||
const newNotificationSettingsEnabled = SettingsStore.getValue(Features.NotificationSettings2);
|
||||
|
||||
return (
|
||||
<SettingsTab>
|
||||
<SettingsSection heading={_t("Notifications")}>
|
||||
<Notifications />
|
||||
</SettingsSection>
|
||||
{newNotificationSettingsEnabled ? (
|
||||
<NotificationSettings2 />
|
||||
) : (
|
||||
<SettingsSection heading={_t("Notifications")}>
|
||||
<Notifications />
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ type UseNotificationSettings = {
|
|||
};
|
||||
|
||||
export function useNotificationSettings(cli: MatrixClient): UseNotificationSettings {
|
||||
const run = useLinearisedPromise<void>();
|
||||
const supportsIntentionalMentions = useMemo(() => cli.supportsIntentionalMentions(), [cli]);
|
||||
|
||||
const pushRules = useRef<IPushRules | null>(null);
|
||||
|
@ -61,21 +62,41 @@ export function useNotificationSettings(cli: MatrixClient): UseNotificationSetti
|
|||
}, [cli, supportsIntentionalMentions]);
|
||||
|
||||
useEffect(() => {
|
||||
updatePushRules().catch((err) => console.error(err));
|
||||
}, [cli, updatePushRules]);
|
||||
run(updatePushRules).catch((err) => console.error(err));
|
||||
}, [cli, run, updatePushRules]);
|
||||
|
||||
const reconcile = useCallback(
|
||||
(model: NotificationSettings) => {
|
||||
if (pushRules.current !== null) {
|
||||
setModel(model);
|
||||
const changes = reconcileNotificationSettings(pushRules.current, model, supportsIntentionalMentions);
|
||||
applyChanges(cli, changes)
|
||||
.then(updatePushRules)
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
setModel(model);
|
||||
run(async () => {
|
||||
if (pushRules.current !== null) {
|
||||
const changes = reconcileNotificationSettings(
|
||||
pushRules.current,
|
||||
model,
|
||||
supportsIntentionalMentions,
|
||||
);
|
||||
await applyChanges(cli, changes);
|
||||
await updatePushRules();
|
||||
}
|
||||
}).catch((err) => console.error(err));
|
||||
},
|
||||
[cli, updatePushRules, supportsIntentionalMentions],
|
||||
[run, supportsIntentionalMentions, cli, updatePushRules],
|
||||
);
|
||||
|
||||
return { model, hasPendingChanges, reconcile };
|
||||
}
|
||||
|
||||
function useLinearisedPromise<T>(): (fun: () => Promise<T>) => Promise<T> {
|
||||
const lastPromise = useRef<Promise<T> | null>(null);
|
||||
|
||||
return useCallback((fun: () => Promise<T>): Promise<T> => {
|
||||
let next: Promise<T>;
|
||||
if (lastPromise.current === null) {
|
||||
next = fun();
|
||||
} else {
|
||||
next = lastPromise.current.then(fun);
|
||||
}
|
||||
lastPromise.current = next;
|
||||
return next;
|
||||
}, []);
|
||||
}
|
||||
|
|
|
@ -953,6 +953,9 @@
|
|||
"Can I use text chat alongside the video call?": "Can I use text chat alongside the video call?",
|
||||
"Yes, the chat timeline is displayed alongside the video.": "Yes, the chat timeline is displayed alongside the video.",
|
||||
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
||||
"New Notification Settings": "New Notification Settings",
|
||||
"Notification Settings": "Notification Settings",
|
||||
"Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.",
|
||||
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
|
||||
"Requires your server to support the stable version of MSC3827": "Requires your server to support the stable version of MSC3827",
|
||||
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
|
||||
|
@ -1767,6 +1770,33 @@
|
|||
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.",
|
||||
"You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.",
|
||||
"Call type": "Call type",
|
||||
"Email Notifications": "Email Notifications",
|
||||
"Email summary": "Email summary",
|
||||
"Receive an email summary of missed notifications": "Receive an email summary of missed notifications",
|
||||
"Select which emails you want to send summaries to. Manage your emails in <button>General</button>.": "Select which emails you want to send summaries to. Manage your emails in <button>General</button>.",
|
||||
"People, Mentions and Keywords": "People, Mentions and Keywords",
|
||||
"Mentions and Keywords only": "Mentions and Keywords only",
|
||||
"Switch now": "Switch now",
|
||||
"<strong>Update:</strong> We have updated our notification settings. This won’t affect your previously selected settings.": "<strong>Update:</strong> We have updated our notification settings. This won’t affect your previously selected settings.",
|
||||
"Show message preview in desktop notification": "Show message preview in desktop notification",
|
||||
"I want to be notified for (Default Setting)": "I want to be notified for (Default Setting)",
|
||||
"This setting will be applied by default to all your rooms.": "This setting will be applied by default to all your rooms.",
|
||||
"Play a sound for": "Play a sound for",
|
||||
"Applied by default to all rooms on all devices.": "Applied by default to all rooms on all devices.",
|
||||
"Mentions and Keywords": "Mentions and Keywords",
|
||||
"Audio and Video calls": "Audio and Video calls",
|
||||
"Other things we think you might be interested in:": "Other things we think you might be interested in:",
|
||||
"Invited to a room": "Invited to a room",
|
||||
"New room activity, upgrades and status messages occur": "New room activity, upgrades and status messages occur",
|
||||
"Messages sent by bots": "Messages sent by bots",
|
||||
"Show a badge <badge/> when keywords are used in a room.": "Show a badge <badge/> when keywords are used in a room.",
|
||||
"Notify when someone mentions using @room": "Notify when someone mentions using @room",
|
||||
"Notify when someone mentions using @displayname or %(mxid)s": "Notify when someone mentions using @displayname or %(mxid)s",
|
||||
"Notify when someone uses a keyword": "Notify when someone uses a keyword",
|
||||
"Enter keywords here, or use for spelling variations or nicknames": "Enter keywords here, or use for spelling variations or nicknames",
|
||||
"Quick Actions": "Quick Actions",
|
||||
"Mark all messages as read": "Mark all messages as read",
|
||||
"Reset to default settings": "Reset to default settings",
|
||||
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
|
||||
"Unable to share email address": "Unable to share email address",
|
||||
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
|
||||
|
|
|
@ -196,6 +196,11 @@ export function reconcileNotificationSettings(
|
|||
}
|
||||
}
|
||||
|
||||
const mentionActions = NotificationUtils.encodeActions({
|
||||
notify: true,
|
||||
sound: model.sound.mentions,
|
||||
highlight: true,
|
||||
});
|
||||
const contentRules = pushRules.global.content?.filter((rule) => !rule.rule_id.startsWith(".")) ?? [];
|
||||
const newKeywords = new Set(model.keywords);
|
||||
for (const rule of contentRules) {
|
||||
|
@ -204,12 +209,27 @@ export function reconcileNotificationSettings(
|
|||
rule_id: rule.rule_id,
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
});
|
||||
} else if (rule.enabled !== model.mentions.keywords) {
|
||||
changes.updated.push({
|
||||
rule_id: rule.rule_id,
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
enabled: model.mentions.keywords,
|
||||
});
|
||||
} else {
|
||||
let changed = false;
|
||||
if (rule.enabled !== model.mentions.keywords) {
|
||||
changed = true;
|
||||
} else if (rule.actions !== undefined) {
|
||||
const originalActions = NotificationUtils.decodeActions(rule.actions);
|
||||
const actions = NotificationUtils.decodeActions(mentionActions);
|
||||
if (originalActions === null || actions === null) {
|
||||
changed = true;
|
||||
} else if (!deepCompare(actions, originalActions)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
changes.updated.push({
|
||||
rule_id: rule.rule_id,
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
enabled: model.mentions.keywords,
|
||||
actions: mentionActions,
|
||||
});
|
||||
}
|
||||
}
|
||||
newKeywords.delete(rule.pattern!);
|
||||
}
|
||||
|
@ -220,7 +240,7 @@ export function reconcileNotificationSettings(
|
|||
default: false,
|
||||
enabled: model.mentions.keywords,
|
||||
pattern: keyword,
|
||||
actions: StandardActions.ACTION_NOTIFY,
|
||||
actions: mentionActions,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,22 @@ function shouldNotify(rules: (IPushRule | null | undefined | false)[]): boolean
|
|||
return false;
|
||||
}
|
||||
|
||||
function isMuted(rules: (IPushRule | null | undefined | false)[]): boolean {
|
||||
if (rules.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (const rule of rules) {
|
||||
if (rule === null || rule === undefined || rule === false || !rule.enabled) {
|
||||
continue;
|
||||
}
|
||||
const actions = NotificationUtils.decodeActions(rule.actions);
|
||||
if (actions !== null && !actions.notify && actions.highlight !== true && actions.sound === undefined) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function determineSound(rules: (IPushRule | null | undefined | false)[]): string | undefined {
|
||||
for (const rule of rules) {
|
||||
if (rule === null || rule === undefined || rule === false || !rule.enabled) {
|
||||
|
@ -74,7 +90,7 @@ export function toNotificationSettings(
|
|||
people: determineSound(dmRules),
|
||||
},
|
||||
activity: {
|
||||
bot_notices: shouldNotify([standardRules.get(RuleId.SuppressNotices)]),
|
||||
bot_notices: !isMuted([standardRules.get(RuleId.SuppressNotices)]),
|
||||
invite: shouldNotify([standardRules.get(RuleId.InviteToSelf)]),
|
||||
status_event: shouldNotify([standardRules.get(RuleId.MemberEvent), standardRules.get(RuleId.Tombstone)]),
|
||||
},
|
||||
|
|
|
@ -94,6 +94,7 @@ export enum LabGroup {
|
|||
export enum Features {
|
||||
VoiceBroadcast = "feature_voice_broadcast",
|
||||
VoiceBroadcastForceSmallChunks = "feature_voice_broadcast_force_small_chunks",
|
||||
NotificationSettings2 = "feature_notification_settings2",
|
||||
OidcNativeFlow = "feature_oidc_native_flow",
|
||||
}
|
||||
|
||||
|
@ -229,6 +230,28 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
|||
requiresRefresh: true,
|
||||
},
|
||||
},
|
||||
[Features.NotificationSettings2]: {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Experimental,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td("New Notification Settings"),
|
||||
default: false,
|
||||
betaInfo: {
|
||||
title: _td("Notification Settings"),
|
||||
caption: () => (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.",
|
||||
{
|
||||
brand: SdkConfig.get().brand,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
"feature_exploring_public_spaces": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Spaces,
|
||||
|
|
|
@ -0,0 +1,762 @@
|
|||
/*
|
||||
Copyright 2022-2023 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 { act, findByRole, getByRole, queryByRole, render, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { IPushRules, MatrixClient, NotificationCountType, PushRuleKind, Room, RuleId } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
|
||||
import NotificationSettings2 from "../../../../../src/components/views/settings/notifications/NotificationSettings2";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { StandardActions } from "../../../../../src/notifications/StandardActions";
|
||||
import { PredictableRandom } from "../../../../predictableRandom";
|
||||
import { mkMessage, stubClient } from "../../../../test-utils";
|
||||
import Mock = jest.Mock;
|
||||
|
||||
const mockRandom = new PredictableRandom();
|
||||
|
||||
// Fake random strings to give a predictable snapshot for IDs
|
||||
jest.mock("matrix-js-sdk/src/randomstring", () => ({
|
||||
randomString: jest.fn((len): string => {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let ret = "";
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
ret += chars.charAt(Math.floor(mockRandom.get() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}),
|
||||
}));
|
||||
|
||||
const waitForUpdate = (): Promise<void> => new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
const labelGlobalMute = "Enable notifications for this account";
|
||||
const labelLevelAllMessage = "All messages";
|
||||
const labelLevelMentionsOnly = "Mentions and Keywords only";
|
||||
const labelSoundPeople = "People";
|
||||
const labelSoundMentions = "Mentions and Keywords";
|
||||
const labelSoundCalls = "Audio and Video calls";
|
||||
const labelActivityInvites = "Invited to a room";
|
||||
const labelActivityStatus = "New room activity, upgrades and status messages occur";
|
||||
const labelActivityBots = "Messages sent by bots";
|
||||
const labelMentionUser = "Notify when someone mentions using @displayname or @mxid";
|
||||
const labelMentionRoom = "Notify when someone mentions using @room";
|
||||
const labelMentionKeyword =
|
||||
"Notify when someone uses a keyword" + "Enter keywords here, or use for spelling variations or nicknames";
|
||||
const labelResetDefault = "Reset to default settings";
|
||||
|
||||
const keywords = ["justjann3", "justj4nn3", "justj4nne", "Janne", "J4nne", "Jann3", "jann3", "j4nne", "janne"];
|
||||
|
||||
describe("<Notifications />", () => {
|
||||
let cli: MatrixClient;
|
||||
let pushRules: IPushRules;
|
||||
|
||||
beforeAll(async () => {
|
||||
pushRules = (await import("../../../../models/notificationsettings/pushrules_sample.json")) as IPushRules;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.safeGet();
|
||||
cli.getPushRules = jest.fn(cli.getPushRules).mockResolvedValue(pushRules);
|
||||
cli.supportsIntentionalMentions = jest.fn(cli.supportsIntentionalMentions).mockReturnValue(false);
|
||||
cli.setPushRuleEnabled = jest.fn(cli.setPushRuleEnabled);
|
||||
cli.setPushRuleActions = jest.fn(cli.setPushRuleActions);
|
||||
cli.addPushRule = jest.fn(cli.addPushRule).mockResolvedValue({});
|
||||
cli.deletePushRule = jest.fn(cli.deletePushRule).mockResolvedValue({});
|
||||
cli.removePusher = jest.fn(cli.removePusher).mockResolvedValue({});
|
||||
cli.setPusher = jest.fn(cli.setPusher).mockResolvedValue({});
|
||||
|
||||
mockRandom.reset();
|
||||
});
|
||||
|
||||
it("matches the snapshot", async () => {
|
||||
cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({
|
||||
pushers: [
|
||||
{
|
||||
app_display_name: "Element",
|
||||
app_id: "im.vector.app",
|
||||
data: {},
|
||||
device_display_name: "My EyeFon",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@example.tld",
|
||||
validated_at: 1656633600,
|
||||
added_at: 1656633600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("correctly handles the loading/disabled state", async () => {
|
||||
(cli.getPushRules as Mock).mockReturnValue(new Promise<IPushRules>(() => {}));
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await waitForUpdate();
|
||||
expect(screen.container).toMatchSnapshot();
|
||||
|
||||
const globalMute = screen.getByLabelText(labelGlobalMute);
|
||||
expect(globalMute).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const levelAllMessages = screen.getByLabelText(labelLevelAllMessage);
|
||||
expect(levelAllMessages).toBeDisabled();
|
||||
|
||||
const soundPeople = screen.getByLabelText(labelSoundPeople);
|
||||
expect(soundPeople).toBeDisabled();
|
||||
const soundMentions = screen.getByLabelText(labelSoundMentions);
|
||||
expect(soundMentions).toBeDisabled();
|
||||
const soundCalls = screen.getByLabelText(labelSoundCalls);
|
||||
expect(soundCalls).toBeDisabled();
|
||||
|
||||
const activityInvites = screen.getByLabelText(labelActivityInvites);
|
||||
expect(activityInvites).toBeDisabled();
|
||||
const activityStatus = screen.getByLabelText(labelActivityStatus);
|
||||
expect(activityStatus).toBeDisabled();
|
||||
const activityBots = screen.getByLabelText(labelActivityBots);
|
||||
expect(activityBots).toBeDisabled();
|
||||
|
||||
const mentionUser = screen.getByLabelText(labelMentionUser.replace("@mxid", cli.getUserId()!));
|
||||
expect(mentionUser).toBeDisabled();
|
||||
const mentionRoom = screen.getByLabelText(labelMentionRoom);
|
||||
expect(mentionRoom).toBeDisabled();
|
||||
const mentionKeyword = screen.getByLabelText(labelMentionKeyword);
|
||||
expect(mentionKeyword).toBeDisabled();
|
||||
await Promise.all([
|
||||
user.click(globalMute),
|
||||
user.click(levelAllMessages),
|
||||
user.click(soundPeople),
|
||||
user.click(soundMentions),
|
||||
user.click(soundCalls),
|
||||
user.click(activityInvites),
|
||||
user.click(activityStatus),
|
||||
user.click(activityBots),
|
||||
user.click(mentionUser),
|
||||
user.click(mentionRoom),
|
||||
user.click(mentionKeyword),
|
||||
]);
|
||||
});
|
||||
|
||||
expect(cli.setPushRuleActions).not.toHaveBeenCalled();
|
||||
expect(cli.setPushRuleEnabled).not.toHaveBeenCalled();
|
||||
expect(cli.addPushRule).not.toHaveBeenCalled();
|
||||
expect(cli.deletePushRule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("form elements actually toggle the model value", () => {
|
||||
it("global mute", async () => {
|
||||
const label = labelGlobalMute;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Override, RuleId.Master, true);
|
||||
});
|
||||
|
||||
it("notification level", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(labelLevelAllMessage)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(labelLevelAllMessage));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedMessage,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true);
|
||||
(cli.setPushRuleEnabled as Mock).mockClear();
|
||||
expect(screen.getByLabelText(labelLevelMentionsOnly)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(labelLevelMentionsOnly));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true);
|
||||
});
|
||||
|
||||
describe("play a sound for", () => {
|
||||
it("people", async () => {
|
||||
const label = labelSoundPeople;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.DM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
});
|
||||
|
||||
it("mentions", async () => {
|
||||
const label = labelSoundMentions;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.ContainsDisplayName,
|
||||
StandardActions.ACTION_HIGHLIGHT,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.ContentSpecific,
|
||||
RuleId.ContainsUserName,
|
||||
StandardActions.ACTION_HIGHLIGHT,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls", async () => {
|
||||
const label = labelSoundCalls;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.IncomingCall,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activity", () => {
|
||||
it("invite", async () => {
|
||||
const label = labelActivityInvites;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
});
|
||||
it("status messages", async () => {
|
||||
const label = labelActivityStatus;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.MemberEvent,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.Tombstone,
|
||||
StandardActions.ACTION_HIGHLIGHT,
|
||||
);
|
||||
});
|
||||
it("notices", async () => {
|
||||
const label = labelActivityBots;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.SuppressNotices,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("mentions", () => {
|
||||
it("room mentions", async () => {
|
||||
const label = labelMentionRoom;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.AtRoomNotification,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
it("user mentions", async () => {
|
||||
const label = labelMentionUser.replace("@mxid", cli.getUserId()!);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.ContainsDisplayName,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.ContentSpecific,
|
||||
RuleId.ContainsUserName,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
});
|
||||
it("keywords", async () => {
|
||||
const label = labelMentionKeyword;
|
||||
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
for (const pattern of keywords) {
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.ContentSpecific,
|
||||
pattern,
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("keywords", () => {
|
||||
it("allows adding keywords", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
const inputField = screen.getByRole("textbox", { name: "Keyword" });
|
||||
const addButton = screen.getByRole("button", { name: "Add" });
|
||||
expect(inputField).not.toBeDisabled();
|
||||
expect(addButton).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.type(inputField, "testkeyword");
|
||||
await user.click(addButton);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.addPushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "testkeyword", {
|
||||
kind: PushRuleKind.ContentSpecific,
|
||||
rule_id: "testkeyword",
|
||||
enabled: true,
|
||||
default: false,
|
||||
actions: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
pattern: "testkeyword",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows deleting keywords", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
const tag = screen.getByText("justj4nn3");
|
||||
const deleteButton = getByRole(tag, "button", { name: "Remove" });
|
||||
expect(deleteButton).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(deleteButton);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, "justj4nn3");
|
||||
});
|
||||
});
|
||||
|
||||
it("resets the model correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
const button = screen.getByText(labelResetDefault);
|
||||
expect(button).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(button);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedMessage,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.Message, true);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
true,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith("global", PushRuleKind.Underride, RuleId.DM, true);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.SuppressNotices,
|
||||
false,
|
||||
);
|
||||
expect(cli.setPushRuleEnabled).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedMessage,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.Message,
|
||||
StandardActions.ACTION_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.EncryptedDM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Underride,
|
||||
RuleId.DM,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.SuppressNotices,
|
||||
StandardActions.ACTION_DONT_NOTIFY,
|
||||
);
|
||||
expect(cli.setPushRuleActions).toHaveBeenCalledWith(
|
||||
"global",
|
||||
PushRuleKind.Override,
|
||||
RuleId.InviteToSelf,
|
||||
StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
);
|
||||
|
||||
for (const pattern of keywords) {
|
||||
expect(cli.deletePushRule).toHaveBeenCalledWith("global", PushRuleKind.ContentSpecific, pattern);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("pusher settings", () => {
|
||||
it("can create email pushers", async () => {
|
||||
cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({
|
||||
pushers: [
|
||||
{
|
||||
app_display_name: "Element",
|
||||
app_id: "im.vector.app",
|
||||
data: {},
|
||||
device_display_name: "My EyeFon",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "",
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@example.tld",
|
||||
validated_at: 1656633600,
|
||||
added_at: 1656633600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const label = "test@example.tld";
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.setPusher).toHaveBeenCalledWith({
|
||||
app_display_name: "Email Notifications",
|
||||
app_id: "m.email",
|
||||
append: true,
|
||||
data: { brand: "Element" },
|
||||
device_display_name: "test@example.tld",
|
||||
kind: "email",
|
||||
lang: "en-US",
|
||||
pushkey: "test@example.tld",
|
||||
});
|
||||
});
|
||||
|
||||
it("can remove email pushers", async () => {
|
||||
cli.getPushers = jest.fn(cli.getPushers).mockResolvedValue({
|
||||
pushers: [
|
||||
{
|
||||
app_display_name: "Element",
|
||||
app_id: "im.vector.app",
|
||||
data: {},
|
||||
device_display_name: "My EyeFon",
|
||||
kind: "http",
|
||||
lang: "en",
|
||||
pushkey: "abctest",
|
||||
},
|
||||
{
|
||||
app_display_name: "Email Notifications",
|
||||
app_id: "m.email",
|
||||
data: { brand: "Element" },
|
||||
device_display_name: "test@example.tld",
|
||||
kind: "email",
|
||||
lang: "en-US",
|
||||
pushkey: "test@example.tld",
|
||||
},
|
||||
],
|
||||
});
|
||||
cli.getThreePids = jest.fn(cli.getThreePids).mockResolvedValue({
|
||||
threepids: [
|
||||
{
|
||||
medium: ThreepidMedium.Email,
|
||||
address: "test@example.tld",
|
||||
validated_at: 1656633600,
|
||||
added_at: 1656633600,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const label = "test@example.tld";
|
||||
const user = userEvent.setup();
|
||||
const screen = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await act(waitForUpdate);
|
||||
expect(screen.getByLabelText(label)).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
await user.click(screen.getByLabelText(label));
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.removePusher).toHaveBeenCalledWith("test@example.tld", "m.email");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear all notifications", () => {
|
||||
it("is hidden when no notifications exist", async () => {
|
||||
const room = new Room("room123", cli, "@alice:example.org");
|
||||
cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]);
|
||||
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await waitForUpdate();
|
||||
expect(
|
||||
queryByRole(container, "button", {
|
||||
name: "Mark all messages as read",
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clears all notifications", async () => {
|
||||
const room = new Room("room123", cli, "@alice:example.org");
|
||||
cli.getRooms = jest.fn(cli.getRooms).mockReturnValue([room]);
|
||||
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: "room123",
|
||||
user: "@alice:example.org",
|
||||
ts: 1,
|
||||
});
|
||||
room.addLiveEvents([message]);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<NotificationSettings2 />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await waitForUpdate();
|
||||
const clearNotificationEl = await findByRole(container, "button", {
|
||||
name: "Mark all messages as read",
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await user.click(clearNotificationEl);
|
||||
await waitForUpdate();
|
||||
});
|
||||
expect(cli.sendReadReceipt).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clearNotificationEl).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -79,6 +79,56 @@ exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BetaCard"
|
||||
>
|
||||
<div
|
||||
class="mx_BetaCard_columns"
|
||||
>
|
||||
<div
|
||||
class="mx_BetaCard_columns_description"
|
||||
>
|
||||
<h3
|
||||
class="mx_BetaCard_title"
|
||||
>
|
||||
<span>
|
||||
Notification Settings
|
||||
</span>
|
||||
<span
|
||||
class="mx_BetaCard_betaPill"
|
||||
>
|
||||
Beta
|
||||
</span>
|
||||
</h3>
|
||||
<div
|
||||
class="mx_BetaCard_caption"
|
||||
>
|
||||
<p>
|
||||
Introducing a simpler way to change your notification settings. Customize your , just the way you like.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BetaCard_buttons"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Join the beta
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BetaCard_columns_image_wrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mx_BetaCard_columns_image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -157,4 +157,37 @@ describe("NotificationSettings", () => {
|
|||
expect(roundtripPendingChanges.deleted).toHaveLength(0);
|
||||
expect(roundtripPendingChanges.updated).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles the bot notice inversion correctly", async () => {
|
||||
const pushRules = (await import("./pushrules_bug_botnotices.json")) as IPushRules;
|
||||
const model = toNotificationSettings(pushRules, false);
|
||||
const pendingChanges = reconcileNotificationSettings(pushRules, model, false);
|
||||
const expectedModel: NotificationSettings = {
|
||||
globalMute: false,
|
||||
defaultLevels: {
|
||||
dm: RoomNotifState.AllMessages,
|
||||
room: RoomNotifState.MentionsOnly,
|
||||
},
|
||||
sound: {
|
||||
calls: "ring",
|
||||
mentions: "default",
|
||||
people: undefined,
|
||||
},
|
||||
activity: {
|
||||
bot_notices: true,
|
||||
invite: true,
|
||||
status_event: false,
|
||||
},
|
||||
mentions: {
|
||||
user: true,
|
||||
room: true,
|
||||
keywords: true,
|
||||
},
|
||||
keywords: ["janne"],
|
||||
};
|
||||
expect(model).toEqual(expectedModel);
|
||||
expect(pendingChanges.added).toHaveLength(0);
|
||||
expect(pendingChanges.deleted).toHaveLength(0);
|
||||
expect(pendingChanges.updated).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
407
test/models/notificationsettings/pushrules_bug_botnotices.json
Normal file
407
test/models/notificationsettings/pushrules_bug_botnotices.json
Normal file
|
@ -0,0 +1,407 @@
|
|||
{
|
||||
"global": {
|
||||
"underride": [
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.call.invite"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "ring"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.call",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.message"
|
||||
},
|
||||
{
|
||||
"kind": "room_member_count",
|
||||
"is": "2"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.room_one_to_one",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.encrypted"
|
||||
},
|
||||
{
|
||||
"kind": "room_member_count",
|
||||
"is": "2"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.encrypted_room_one_to_one",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.message"
|
||||
}
|
||||
],
|
||||
"actions": ["dont_notify"],
|
||||
"rule_id": ".m.rule.message",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.encrypted"
|
||||
}
|
||||
],
|
||||
"actions": ["dont_notify"],
|
||||
"rule_id": ".m.rule.encrypted",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "im.vector.modular.widgets"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "content.type",
|
||||
"pattern": "jitsi"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "state_key",
|
||||
"pattern": "*"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"rule_id": ".im.vector.jitsi",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"rule_id": ".org.matrix.msc3914.rule.room.call",
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "org.matrix.msc3401.call"
|
||||
},
|
||||
{
|
||||
"kind": "call_started"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sender": [],
|
||||
"room": [],
|
||||
"content": [
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
],
|
||||
"pattern": "janne",
|
||||
"rule_id": "janne",
|
||||
"default": false,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.contains_user_name",
|
||||
"default": true,
|
||||
"pattern": "jannemk",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"override": [
|
||||
{
|
||||
"conditions": [],
|
||||
"actions": ["dont_notify"],
|
||||
"rule_id": ".m.rule.master",
|
||||
"default": true,
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "content.msgtype",
|
||||
"pattern": "m.notice"
|
||||
}
|
||||
],
|
||||
"actions": ["dont_notify"],
|
||||
"rule_id": ".m.rule.suppress_notices",
|
||||
"default": true,
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.member"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "content.membership",
|
||||
"pattern": "invite"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "state_key",
|
||||
"pattern": "@jannemk:element.io"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.invite_for_me",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.member"
|
||||
}
|
||||
],
|
||||
"actions": ["dont_notify"],
|
||||
"rule_id": ".m.rule.member_event",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "contains_display_name"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "sound",
|
||||
"value": "default"
|
||||
},
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.contains_display_name",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "sender_notification_permission",
|
||||
"key": "room"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "content.body",
|
||||
"pattern": "@room"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.roomnotif",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.tombstone"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "state_key",
|
||||
"pattern": ""
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
],
|
||||
"rule_id": ".m.rule.tombstone",
|
||||
"default": true,
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.reaction"
|
||||
}
|
||||
],
|
||||
"actions": ["dont_notify"],
|
||||
"rule_id": ".m.rule.reaction",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.server_acl"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "state_key",
|
||||
"pattern": ""
|
||||
}
|
||||
],
|
||||
"actions": [],
|
||||
"rule_id": ".m.rule.room.server_acl",
|
||||
"default": true,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"rule_id": ".org.matrix.msc3952.is_user_mention",
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_property_contains",
|
||||
"key": "content.org\\.matrix\\.msc3952\\.mentions.user_ids",
|
||||
"value": "@jannemk:element.io"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rule_id": ".org.matrix.msc3952.is_room_mention",
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_property_is",
|
||||
"key": "content.org\\.matrix\\.msc3952\\.mentions.room",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"kind": "sender_notification_permission",
|
||||
"key": "room"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"notify",
|
||||
{
|
||||
"set_tweak": "highlight"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rule_id": ".org.matrix.msc3786.rule.room.server_acl",
|
||||
"default": true,
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "type",
|
||||
"pattern": "m.room.server_acl"
|
||||
},
|
||||
{
|
||||
"kind": "event_match",
|
||||
"key": "state_key",
|
||||
"pattern": ""
|
||||
}
|
||||
],
|
||||
"actions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
34
test/predictableRandom.ts
Normal file
34
test/predictableRandom.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
// Fake random strings to give a predictable snapshot for IDs
|
||||
// Simple Xorshift random number generator with predictable ID
|
||||
export class PredictableRandom {
|
||||
private state: number;
|
||||
|
||||
constructor() {
|
||||
this.state = 314159265;
|
||||
}
|
||||
|
||||
get(): number {
|
||||
this.state ^= this.state << 13;
|
||||
this.state ^= this.state >> 17;
|
||||
this.state ^= this.state << 5;
|
||||
return this.state / 1073741823;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.state = 314159265;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue