Merge remote-tracking branch 'upstream/develop' into task/cleanup-replies
This commit is contained in:
commit
75fc1299fb
46 changed files with 1349 additions and 1563 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -198,8 +198,9 @@ $irc-line-height: $font-18px;
|
||||||
.mx_ReplyThread {
|
.mx_ReplyThread {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
.mx_SenderProfile {
|
.mx_SenderProfile {
|
||||||
|
order: unset;
|
||||||
|
max-width: unset;
|
||||||
width: unset;
|
width: unset;
|
||||||
max-width: var(--name-width);
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -193,6 +193,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomTile_iconCopyLink::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomTile_iconInvite::before {
|
.mx_RoomTile_iconInvite::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
|
@ -25,7 +25,6 @@ import _linkifyElement from 'linkifyjs/element';
|
||||||
import _linkifyString from 'linkifyjs/string';
|
import _linkifyString from 'linkifyjs/string';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import url from 'url';
|
|
||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
import { AllHtmlEntities } from 'html-entities';
|
||||||
import { IContent } from 'matrix-js-sdk/src/models/event';
|
import { IContent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
@ -153,10 +152,8 @@ export function getHtmlText(insaneHtml: string): string {
|
||||||
*/
|
*/
|
||||||
export function isUrlPermitted(inputUrl: string): boolean {
|
export function isUrlPermitted(inputUrl: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsed = url.parse(inputUrl);
|
|
||||||
if (!parsed.protocol) return false;
|
|
||||||
// URL parser protocol includes the trailing colon
|
// URL parser protocol includes the trailing colon
|
||||||
return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1));
|
return PERMITTED_URL_SCHEMES.includes(new URL(inputUrl).protocol.slice(0, -1));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
|
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
|
||||||
|
import { QueryDict } from 'matrix-js-sdk/src/utils';
|
||||||
|
|
||||||
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
|
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
@ -65,7 +66,7 @@ interface ILoadSessionOpts {
|
||||||
guestIsUrl?: string;
|
guestIsUrl?: string;
|
||||||
ignoreGuest?: boolean;
|
ignoreGuest?: boolean;
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
fragmentQueryParams?: Record<string, string>;
|
fragmentQueryParams?: QueryDict;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,8 +119,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||||
) {
|
) {
|
||||||
console.log("Using guest access credentials");
|
console.log("Using guest access credentials");
|
||||||
return doSetLoggedIn({
|
return doSetLoggedIn({
|
||||||
userId: fragmentQueryParams.guest_user_id,
|
userId: fragmentQueryParams.guest_user_id as string,
|
||||||
accessToken: fragmentQueryParams.guest_access_token,
|
accessToken: fragmentQueryParams.guest_access_token as string,
|
||||||
homeserverUrl: guestHsUrl,
|
homeserverUrl: guestHsUrl,
|
||||||
identityServerUrl: guestIsUrl,
|
identityServerUrl: guestIsUrl,
|
||||||
guest: true,
|
guest: true,
|
||||||
|
@ -173,7 +174,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
||||||
* login, else false
|
* login, else false
|
||||||
*/
|
*/
|
||||||
export function attemptTokenLogin(
|
export function attemptTokenLogin(
|
||||||
queryParams: Record<string, string>,
|
queryParams: QueryDict,
|
||||||
defaultDeviceDisplayName?: string,
|
defaultDeviceDisplayName?: string,
|
||||||
fragmentAfterLogin?: string,
|
fragmentAfterLogin?: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
@ -198,7 +199,7 @@ export function attemptTokenLogin(
|
||||||
homeserver,
|
homeserver,
|
||||||
identityServer,
|
identityServer,
|
||||||
"m.login.token", {
|
"m.login.token", {
|
||||||
token: queryParams.loginToken,
|
token: queryParams.loginToken as string,
|
||||||
initial_device_display_name: defaultDeviceDisplayName,
|
initial_device_display_name: defaultDeviceDisplayName,
|
||||||
},
|
},
|
||||||
).then(function(creds) {
|
).then(function(creds) {
|
||||||
|
|
|
@ -328,7 +328,7 @@ export const Notifier = {
|
||||||
|
|
||||||
onEvent: function(ev: MatrixEvent) {
|
onEvent: function(ev: MatrixEvent) {
|
||||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||||
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
MatrixClientPeg.get().decryptEventIfNeeded(ev);
|
MatrixClientPeg.get().decryptEventIfNeeded(ev);
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
@ -32,7 +31,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
// any text to display at all. For this reason they return deferred values
|
// any text to display at all. For this reason they return deferred values
|
||||||
// to avoid the expense of looking up translations when they're not needed.
|
// to avoid the expense of looking up translations when they're not needed.
|
||||||
|
|
||||||
function textForMemberEvent(ev: MatrixEvent): () => string | null {
|
function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null {
|
||||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
|
@ -84,7 +83,7 @@ function textForMemberEvent(ev: MatrixEvent): () => string | null {
|
||||||
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
||||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
return () => _t('%(senderName)s set a profile picture', { senderName });
|
return () => _t('%(senderName)s set a profile picture', { senderName });
|
||||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
} else if (showHiddenEvents ?? SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
||||||
return () => _t("%(senderName)s made no change", { senderName });
|
return () => _t("%(senderName)s made no change", { senderName });
|
||||||
} else {
|
} else {
|
||||||
|
@ -319,7 +318,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallAnswerEvent(event): () => string | null {
|
function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
|
@ -327,7 +326,7 @@ function textForCallAnswerEvent(event): () => string | null {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallHangupEvent(event): () => string | null {
|
function textForCallHangupEvent(event: MatrixEvent): () => string | null {
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
const eventContent = event.getContent();
|
const eventContent = event.getContent();
|
||||||
let getReason = () => "";
|
let getReason = () => "";
|
||||||
|
@ -364,14 +363,14 @@ function textForCallHangupEvent(event): () => string | null {
|
||||||
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallRejectEvent(event): () => string | null {
|
function textForCallRejectEvent(event: MatrixEvent): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
return _t('%(senderName)s declined the call.', { senderName });
|
return _t('%(senderName)s declined the call.', { senderName });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallInviteEvent(event): () => string | null {
|
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
|
||||||
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
let isVoice = true;
|
let isVoice = true;
|
||||||
|
@ -403,7 +402,7 @@ function textForCallInviteEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForThreePidInviteEvent(event): () => string | null {
|
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
|
|
||||||
if (!isValid3pidInvite(event)) {
|
if (!isValid3pidInvite(event)) {
|
||||||
|
@ -419,7 +418,7 @@ function textForThreePidInviteEvent(event): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForHistoryVisibilityEvent(event): () => string | null {
|
function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
switch (event.getContent().history_visibility) {
|
switch (event.getContent().history_visibility) {
|
||||||
case 'invited':
|
case 'invited':
|
||||||
|
@ -441,7 +440,7 @@ function textForHistoryVisibilityEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently will only display a change if a user's power level is changed
|
// Currently will only display a change if a user's power level is changed
|
||||||
function textForPowerEvent(event): () => string | null {
|
function textForPowerEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
if (!event.getPrevContent() || !event.getPrevContent().users ||
|
||||||
!event.getContent() || !event.getContent().users) {
|
!event.getContent() || !event.getContent().users) {
|
||||||
|
@ -523,7 +522,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string
|
||||||
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
|
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetEvent(event): () => string | null {
|
function textForWidgetEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
|
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
|
||||||
const { name, type, url } = event.getContent() || {};
|
const { name, type, url } = event.getContent() || {};
|
||||||
|
@ -553,12 +552,12 @@ function textForWidgetEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetLayoutEvent(event): () => string | null {
|
function textForWidgetLayoutEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.sender?.name || event.getSender();
|
const senderName = event.sender?.name || event.getSender();
|
||||||
return () => _t("%(senderName)s has updated the widget layout", { senderName });
|
return () => _t("%(senderName)s has updated the widget layout", { senderName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMjolnirEvent(event): () => string | null {
|
function textForMjolnirEvent(event: MatrixEvent): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const { entity: prevEntity } = event.getPrevContent();
|
const { entity: prevEntity } = event.getPrevContent();
|
||||||
const { entity, recommendation, reason } = event.getContent();
|
const { entity, recommendation, reason } = event.getContent();
|
||||||
|
@ -646,7 +645,9 @@ function textForMjolnirEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHandlers {
|
interface IHandlers {
|
||||||
[type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null);
|
[type: string]:
|
||||||
|
(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) =>
|
||||||
|
(() => string | JSX.Element | null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers: IHandlers = {
|
const handlers: IHandlers = {
|
||||||
|
@ -682,14 +683,27 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateHandlers[evType] = textForMjolnirEvent;
|
stateHandlers[evType] = textForMjolnirEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasText(ev: MatrixEvent): boolean {
|
/**
|
||||||
|
* Determines whether the given event has text to display.
|
||||||
|
* @param ev The event
|
||||||
|
* @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
|
||||||
|
* to avoid hitting the settings store
|
||||||
|
*/
|
||||||
|
export function hasText(ev: MatrixEvent, showHiddenEvents?: boolean): boolean {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
return Boolean(handler?.(ev));
|
return Boolean(handler?.(ev, false, showHiddenEvents));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the textual content of the given event.
|
||||||
|
* @param ev The event
|
||||||
|
* @param allowJSX Whether to output rich JSX content
|
||||||
|
* @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
|
||||||
|
* to avoid hitting the settings store
|
||||||
|
*/
|
||||||
export function textForEvent(ev: MatrixEvent): string;
|
export function textForEvent(ev: MatrixEvent): string;
|
||||||
export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element;
|
export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
|
||||||
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element {
|
export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
return handler?.(ev, allowJSX)?.() || '';
|
return handler?.(ev, allowJSX, showHiddenEvents)?.() || '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { haveTileForEvent } from "./components/views/rooms/EventTile";
|
||||||
* @returns {boolean} True if the given event should affect the unread message count
|
* @returns {boolean} True if the given event should affect the unread message count
|
||||||
*/
|
*/
|
||||||
export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (ev.getSender() === MatrixClientPeg.get().credentials.userId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,9 +63,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
// https://github.com/vector-im/element-web/issues/2427
|
// https://github.com/vector-im/element-web/issues/2427
|
||||||
// ...and possibly some of the others at
|
// ...and possibly some of the others at
|
||||||
// https://github.com/vector-im/element-web/issues/3363
|
// https://github.com/vector-im/element-web/issues/3363
|
||||||
if (room.timeline.length &&
|
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
||||||
room.timeline[room.timeline.length - 1].sender &&
|
|
||||||
room.timeline[room.timeline.length - 1].sender.userId === myUserId) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { createClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { sleep, defer, IDeferred } from "matrix-js-sdk/src/utils";
|
import { sleep, defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||||
import 'focus-visible';
|
import 'focus-visible';
|
||||||
|
@ -105,6 +105,8 @@ import VerificationRequestToast from '../views/toasts/VerificationRequestToast';
|
||||||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||||
import SoftLogout from './auth/SoftLogout';
|
import SoftLogout from './auth/SoftLogout';
|
||||||
|
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||||
|
import { copyPlaintext } from "../../utils/strings";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
export enum Views {
|
export enum Views {
|
||||||
|
@ -153,7 +155,7 @@ const ONBOARDING_FLOW_STARTERS = [
|
||||||
|
|
||||||
interface IScreen {
|
interface IScreen {
|
||||||
screen: string;
|
screen: string;
|
||||||
params?: object;
|
params?: QueryDict;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -183,9 +185,9 @@ interface IProps { // TODO type things better
|
||||||
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
||||||
enableGuest?: boolean;
|
enableGuest?: boolean;
|
||||||
// the queryParams extracted from the [real] query-string of the URI
|
// the queryParams extracted from the [real] query-string of the URI
|
||||||
realQueryParams?: Record<string, string>;
|
realQueryParams?: QueryDict;
|
||||||
// the initial queryParams extracted from the hash-fragment of the URI
|
// the initial queryParams extracted from the hash-fragment of the URI
|
||||||
startingFragmentQueryParams?: Record<string, string>;
|
startingFragmentQueryParams?: QueryDict;
|
||||||
// called when we have completed a token login
|
// called when we have completed a token login
|
||||||
onTokenLoginCompleted?: () => void;
|
onTokenLoginCompleted?: () => void;
|
||||||
// Represents the screen to display as a result of parsing the initial window.location
|
// Represents the screen to display as a result of parsing the initial window.location
|
||||||
|
@ -193,7 +195,7 @@ interface IProps { // TODO type things better
|
||||||
// displayname, if any, to set on the device when logging in/registering.
|
// displayname, if any, to set on the device when logging in/registering.
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
// A function that makes a registration URL
|
// A function that makes a registration URL
|
||||||
makeRegistrationUrl: (object) => string;
|
makeRegistrationUrl: (params: QueryDict) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -296,7 +298,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) {
|
if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) {
|
||||||
// probably a threepid invite - try to store it
|
// probably a threepid invite - try to store it
|
||||||
const roomId = this.screenAfterLogin.screen.substring("room/".length);
|
const roomId = this.screenAfterLogin.screen.substring("room/".length);
|
||||||
ThreepidInviteStore.instance.storeInvite(roomId, params as IThreepidInviteWireFormat);
|
ThreepidInviteStore.instance.storeInvite(roomId, params as unknown as IThreepidInviteWireFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,6 +629,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
case 'forget_room':
|
case 'forget_room':
|
||||||
this.forgetRoom(payload.room_id);
|
this.forgetRoom(payload.room_id);
|
||||||
break;
|
break;
|
||||||
|
case 'copy_room':
|
||||||
|
this.copyRoom(payload.room_id);
|
||||||
|
break;
|
||||||
case 'reject_invite':
|
case 'reject_invite':
|
||||||
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
|
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
|
||||||
title: _t('Reject invitation'),
|
title: _t('Reject invitation'),
|
||||||
|
@ -1193,6 +1198,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async copyRoom(roomId: string) {
|
||||||
|
const roomLink = makeRoomPermalink(roomId);
|
||||||
|
const success = await copyPlaintext(roomLink);
|
||||||
|
if (!success) {
|
||||||
|
Modal.createTrackedDialog("Unable to copy room link", "", ErrorDialog, {
|
||||||
|
title: _t("Unable to copy room link"),
|
||||||
|
description: _t("Unable to copy a link to the room to the clipboard."),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a chat with the welcome user, if the user doesn't already have one
|
* Starts a chat with the welcome user, if the user doesn't already have one
|
||||||
* @returns {string} The room ID of the new room, or null if no room was created
|
* @returns {string} The room ID of the new room, or null if no room was created
|
||||||
|
@ -1936,7 +1952,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ serverConfig });
|
this.setState({ serverConfig });
|
||||||
};
|
};
|
||||||
|
|
||||||
private makeRegistrationUrl = (params: {[key: string]: string}) => {
|
private makeRegistrationUrl = (params: QueryDict) => {
|
||||||
if (this.props.startingFragmentQueryParams.referrer) {
|
if (this.props.startingFragmentQueryParams.referrer) {
|
||||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||||
}
|
}
|
||||||
|
@ -2091,7 +2107,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
|
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
fragmentAfterLogin={fragmentAfterLogin}
|
fragmentAfterLogin={fragmentAfterLogin}
|
||||||
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername}
|
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername as string}
|
||||||
{...this.getServerProperties()}
|
{...this.getServerProperties()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -54,7 +54,11 @@ const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, E
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): boolean {
|
function shouldFormContinuation(
|
||||||
|
prevEvent: MatrixEvent,
|
||||||
|
mxEvent: MatrixEvent,
|
||||||
|
showHiddenEvents: boolean,
|
||||||
|
): boolean {
|
||||||
// sanity check inputs
|
// sanity check inputs
|
||||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||||
// check if within the max continuation period
|
// check if within the max continuation period
|
||||||
|
@ -74,7 +78,7 @@ function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): b
|
||||||
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
||||||
|
|
||||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||||
if (!haveTileForEvent(prevEvent)) return false;
|
if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -239,7 +243,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache hidden events setting on mount since Settings is expensive to
|
// Cache hidden events setting on mount since Settings is expensive to
|
||||||
// query, and we check this in a hot code path.
|
// query, and we check this in a hot code path. This is also cached in
|
||||||
|
// our RoomContext, however we still need a fallback for roomless MessagePanels.
|
||||||
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
|
this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||||
|
|
||||||
this.showTypingNotificationsWatcherRef =
|
this.showTypingNotificationsWatcherRef =
|
||||||
|
@ -399,17 +404,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
return !this.isMounted;
|
return !this.isMounted;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get showHiddenEvents(): boolean {
|
||||||
|
return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Implement granular (per-room) hide options
|
// TODO: Implement granular (per-room) hide options
|
||||||
public shouldShowEvent(mxEv: MatrixEvent): boolean {
|
public shouldShowEvent(mxEv: MatrixEvent): boolean {
|
||||||
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) {
|
||||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showHiddenEventsInTimeline) {
|
if (this.showHiddenEvents) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!haveTileForEvent(mxEv)) {
|
if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
|
||||||
return false; // no tile = no show
|
return false; // no tile = no show
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,7 +578,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
if (grouper) {
|
if (grouper) {
|
||||||
if (grouper.shouldGroup(mxEv)) {
|
if (grouper.shouldGroup(mxEv)) {
|
||||||
grouper.add(mxEv);
|
grouper.add(mxEv, this.showHiddenEvents);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// not part of group, so get the group tiles, close the
|
// not part of group, so get the group tiles, close the
|
||||||
|
@ -649,7 +658,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
// is this a continuation of the previous message?
|
||||||
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
|
const continuation = !wantsDateSeparator &&
|
||||||
|
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
|
||||||
|
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const highlight = (eventId === this.props.highlightedEventId);
|
const highlight = (eventId === this.props.highlightedEventId);
|
||||||
|
@ -946,7 +956,7 @@ abstract class BaseGrouper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract shouldGroup(ev: MatrixEvent): boolean;
|
public abstract shouldGroup(ev: MatrixEvent): boolean;
|
||||||
public abstract add(ev: MatrixEvent): void;
|
public abstract add(ev: MatrixEvent, showHiddenEvents?: boolean): void;
|
||||||
public abstract getTiles(): ReactNode[];
|
public abstract getTiles(): ReactNode[];
|
||||||
public abstract getNewPrevEvent(): MatrixEvent;
|
public abstract getNewPrevEvent(): MatrixEvent;
|
||||||
}
|
}
|
||||||
|
@ -1200,10 +1210,10 @@ class MemberGrouper extends BaseGrouper {
|
||||||
return membershipTypes.includes(ev.getType() as EventType);
|
return membershipTypes.includes(ev.getType() as EventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(ev: MatrixEvent): void {
|
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||||
if (ev.getType() === EventType.RoomMember) {
|
if (ev.getType() === EventType.RoomMember) {
|
||||||
// We can ignore any events that don't actually have a message to display
|
// We can ignore any events that don't actually have a message to display
|
||||||
if (!hasText(ev)) return;
|
if (!hasText(ev, showHiddenEvents)) return;
|
||||||
}
|
}
|
||||||
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
|
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
|
||||||
ev.getId(),
|
ev.getId(),
|
||||||
|
|
|
@ -166,6 +166,7 @@ export interface IState {
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
lowBandwidth: boolean;
|
lowBandwidth: boolean;
|
||||||
|
showHiddenEventsInTimeline: boolean;
|
||||||
showReadReceipts: boolean;
|
showReadReceipts: boolean;
|
||||||
showRedactions: boolean;
|
showRedactions: boolean;
|
||||||
showJoinLeaves: boolean;
|
showJoinLeaves: boolean;
|
||||||
|
@ -230,6 +231,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: SettingsStore.getValue("layout"),
|
layout: SettingsStore.getValue("layout"),
|
||||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||||
|
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
showRedactions: true,
|
showRedactions: true,
|
||||||
showJoinLeaves: true,
|
showJoinLeaves: true,
|
||||||
|
@ -253,7 +255,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||||
this.context.on("Event.decrypted", this.onEventDecrypted);
|
this.context.on("Event.decrypted", this.onEventDecrypted);
|
||||||
this.context.on("event", this.onEvent);
|
|
||||||
// Start listening for RoomViewStore updates
|
// Start listening for RoomViewStore updates
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||||
|
@ -268,6 +269,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||||
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
|
||||||
),
|
),
|
||||||
|
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
|
||||||
|
this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -637,7 +641,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||||
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||||
this.context.removeListener("Event.decrypted", this.onEventDecrypted);
|
this.context.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||||
this.context.removeListener("event", this.onEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||||
|
@ -837,8 +840,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
// ignore events for other rooms
|
// ignore events for other rooms
|
||||||
if (!room) return;
|
if (!room || room.roomId !== this.state.room?.roomId) return;
|
||||||
if (!this.state.room || room.roomId != this.state.room.roomId) return;
|
|
||||||
|
|
||||||
// ignore events from filtered timelines
|
// ignore events from filtered timelines
|
||||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||||
|
@ -859,6 +861,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// we'll only be showing a spinner.
|
// we'll only be showing a spinner.
|
||||||
if (this.state.joining) return;
|
if (this.state.joining) return;
|
||||||
|
|
||||||
|
if (!ev.isBeingDecrypted() && !ev.isDecryptionFailure()) {
|
||||||
|
this.handleEffects(ev);
|
||||||
|
}
|
||||||
|
|
||||||
if (ev.getSender() !== this.context.credentials.userId) {
|
if (ev.getSender() !== this.context.credentials.userId) {
|
||||||
// update unread count when scrolled up
|
// update unread count when scrolled up
|
||||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||||
|
@ -871,20 +877,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEventDecrypted = (ev) => {
|
private onEventDecrypted = (ev: MatrixEvent) => {
|
||||||
|
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
|
||||||
|
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
|
||||||
if (ev.isDecryptionFailure()) return;
|
if (ev.isDecryptionFailure()) return;
|
||||||
this.handleEffects(ev);
|
this.handleEffects(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEvent = (ev) => {
|
private handleEffects = (ev: MatrixEvent) => {
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
|
||||||
this.handleEffects(ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleEffects = (ev) => {
|
|
||||||
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
|
|
||||||
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
|
|
||||||
|
|
||||||
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
|
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
|
||||||
if (!notifState.isUnread) return;
|
if (!notifState.isUnread) return;
|
||||||
|
|
||||||
|
@ -1393,7 +1393,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!haveTileForEvent(mxEv)) {
|
if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
|
||||||
// XXX: can this ever happen? It will make the result count
|
// XXX: can this ever happen? It will make the result count
|
||||||
// not match the displayed count.
|
// not match the displayed count.
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -555,9 +555,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
// more than the timeout on userActiveRecently.
|
// more than the timeout on userActiveRecently.
|
||||||
//
|
//
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
const sender = ev.sender ? ev.sender.userId : null;
|
|
||||||
callRMUpdated = false;
|
callRMUpdated = false;
|
||||||
if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
||||||
updatedState.readMarkerVisible = true;
|
updatedState.readMarkerVisible = true;
|
||||||
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
|
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
|
||||||
// we know we're stuckAtBottom, so we can advance the RM
|
// we know we're stuckAtBottom, so we can advance the RM
|
||||||
|
@ -863,7 +862,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
for (i++; i < events.length; i++) {
|
for (i++; i < events.length; i++) {
|
||||||
const ev = events[i];
|
const ev = events[i];
|
||||||
if (!ev.sender || ev.sender.userId != myUserId) {
|
if (ev.getSender() !== myUserId) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1337,8 +1336,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldIgnore = !!ev.status || // local echo
|
const shouldIgnore = !!ev.status || // local echo
|
||||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
(ignoreOwn && ev.getSender() === myUserId); // own message
|
||||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
|
||||||
|
shouldHideEvent(ev, this.context);
|
||||||
|
|
||||||
if (isWithoutTile || !node) {
|
if (isWithoutTile || !node) {
|
||||||
// don't start counting if the event should be ignored,
|
// don't start counting if the event should be ignored,
|
||||||
|
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import * as TextForEvent from "../../../TextForEvent";
|
import * as TextForEvent from "../../../TextForEvent";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -26,11 +27,11 @@ interface IProps {
|
||||||
|
|
||||||
@replaceableComponent("views.messages.TextualEvent")
|
@replaceableComponent("views.messages.TextualEvent")
|
||||||
export default class TextualEvent extends React.Component<IProps> {
|
export default class TextualEvent extends React.Component<IProps> {
|
||||||
render() {
|
static contextType = RoomContext;
|
||||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
|
|
||||||
if (!text || (text as string).length === 0) return null;
|
public render() {
|
||||||
return (
|
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
|
||||||
<div className="mx_TextualEvent">{ text }</div>
|
if (!text) return null;
|
||||||
);
|
return <div className="mx_TextualEvent">{ text }</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1160,7 +1160,7 @@ function isMessageEvent(ev) {
|
||||||
return (messageTypes.includes(ev.getType()));
|
return (messageTypes.includes(ev.getType()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function haveTileForEvent(e) {
|
export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) {
|
||||||
// Only messages have a tile (black-rectangle) if redacted
|
// Only messages have a tile (black-rectangle) if redacted
|
||||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||||
|
|
||||||
|
@ -1170,7 +1170,7 @@ export function haveTileForEvent(e) {
|
||||||
const handler = getHandlerTile(e);
|
const handler = getHandlerTile(e);
|
||||||
if (handler === undefined) return false;
|
if (handler === undefined) return false;
|
||||||
if (handler === 'messages.TextualEvent') {
|
if (handler === 'messages.TextualEvent') {
|
||||||
return hasText(e);
|
return hasText(e, showHiddenEvents);
|
||||||
} else if (handler === 'messages.RoomCreate') {
|
} else if (handler === 'messages.RoomCreate') {
|
||||||
return Boolean(e.getContent()['predecessor']);
|
return Boolean(e.getContent()['predecessor']);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -408,10 +408,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
this.setState({ addRoomContextMenuPosition: null });
|
this.setState({ addRoomContextMenuPosition: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUnreadFirstChanged = async () => {
|
private onUnreadFirstChanged = () => {
|
||||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||||
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
||||||
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
||||||
this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change
|
this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -358,6 +358,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.setState({ generalMenuPosition: null }); // hide the menu
|
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onCopyRoomClick = (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'copy_room',
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
});
|
||||||
|
this.setState({ generalMenuPosition: null }); // hide the menu
|
||||||
|
};
|
||||||
|
|
||||||
private onInviteClick = (ev: ButtonEvent) => {
|
private onInviteClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -408,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}
|
||||||
|
@ -517,6 +528,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
iconClassName="mx_RoomTile_iconInvite"
|
iconClassName="mx_RoomTile_iconInvite"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
onClick={this.onCopyRoomClick}
|
||||||
|
label={_t("Copy Link")}
|
||||||
|
iconClassName="mx_RoomTile_iconCopyLink"
|
||||||
|
/>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
onClick={this.onOpenRoomSettings}
|
onClick={this.onOpenRoomSettings}
|
||||||
label={_t("Settings")}
|
label={_t("Settings")}
|
||||||
|
|
|
@ -15,14 +15,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||||
import EventTile, { haveTileForEvent } from "./EventTile";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import DateSeparator from '../messages/DateSeparator';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import DateSeparator from "../messages/DateSeparator";
|
||||||
|
import EventTile, { haveTileForEvent } from "./EventTile";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// a matrix-js-sdk SearchResult containing the details of this result
|
// a matrix-js-sdk SearchResult containing the details of this result
|
||||||
|
@ -37,6 +38,8 @@ interface IProps {
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.SearchResultTile")
|
@replaceableComponent("views.rooms.SearchResultTile")
|
||||||
export default class SearchResultTile extends React.Component<IProps> {
|
export default class SearchResultTile extends React.Component<IProps> {
|
||||||
|
static contextType = RoomContext;
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const result = this.props.searchResult;
|
const result = this.props.searchResult;
|
||||||
const mxEv = result.context.getEvent();
|
const mxEv = result.context.getEvent();
|
||||||
|
@ -44,7 +47,10 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||||
|
|
||||||
const ts1 = mxEv.getTs();
|
const ts1 = mxEv.getTs();
|
||||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||||
|
const layout = SettingsStore.getValue("layout");
|
||||||
|
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||||
|
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
|
||||||
|
|
||||||
const timeline = result.context.getTimeline();
|
const timeline = result.context.getTimeline();
|
||||||
for (let j = 0; j < timeline.length; j++) {
|
for (let j = 0; j < timeline.length; j++) {
|
||||||
|
@ -54,26 +60,25 @@ export default class SearchResultTile extends React.Component<IProps> {
|
||||||
if (!contextual) {
|
if (!contextual) {
|
||||||
highlights = this.props.searchHighlights;
|
highlights = this.props.searchHighlights;
|
||||||
}
|
}
|
||||||
if (haveTileForEvent(ev)) {
|
if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
|
||||||
ret.push((
|
ret.push(
|
||||||
<EventTile
|
<EventTile
|
||||||
key={`${eventId}+${j}`}
|
key={`${eventId}+${j}`}
|
||||||
mxEvent={ev}
|
mxEvent={ev}
|
||||||
|
layout={layout}
|
||||||
contextual={contextual}
|
contextual={contextual}
|
||||||
highlights={highlights}
|
highlights={highlights}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
highlightLink={this.props.resultLink}
|
highlightLink={this.props.resultLink}
|
||||||
onHeightChanged={this.props.onHeightChanged}
|
onHeightChanged={this.props.onHeightChanged}
|
||||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
isTwelveHour={isTwelveHour}
|
||||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={enableFlair}
|
||||||
/>
|
/>,
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<li data-scroll-tokens={eventId}>
|
return <li data-scroll-tokens={eventId}>{ ret }</li>;
|
||||||
{ ret }
|
|
||||||
</li>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -41,6 +41,7 @@ const RoomContext = createContext<IState>({
|
||||||
canReply: false,
|
canReply: false,
|
||||||
layout: Layout.Group,
|
layout: Layout.Group,
|
||||||
lowBandwidth: false,
|
lowBandwidth: false,
|
||||||
|
showHiddenEventsInTimeline: false,
|
||||||
showReadReceipts: true,
|
showReadReceipts: true,
|
||||||
showRedactions: true,
|
showRedactions: true,
|
||||||
showJoinLeaves: true,
|
showJoinLeaves: true,
|
||||||
|
|
|
@ -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",
|
||||||
|
@ -1667,6 +1657,7 @@
|
||||||
"Favourite": "Favourite",
|
"Favourite": "Favourite",
|
||||||
"Low Priority": "Low Priority",
|
"Low Priority": "Low Priority",
|
||||||
"Invite People": "Invite People",
|
"Invite People": "Invite People",
|
||||||
|
"Copy Link": "Copy Link",
|
||||||
"Leave Room": "Leave Room",
|
"Leave Room": "Leave Room",
|
||||||
"Room options": "Room options",
|
"Room options": "Room options",
|
||||||
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
||||||
|
@ -2674,6 +2665,8 @@
|
||||||
"Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?",
|
"Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?",
|
||||||
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
|
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
|
||||||
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
|
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
|
||||||
|
"Unable to copy room link": "Unable to copy room link",
|
||||||
|
"Unable to copy a link to the room to the clipboard.": "Unable to copy a link to the room to the clipboard.",
|
||||||
"Signed Out": "Signed Out",
|
"Signed Out": "Signed Out",
|
||||||
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
|
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
|
||||||
"Terms and Conditions": "Terms and Conditions",
|
"Terms and Conditions": "Terms and Conditions",
|
||||||
|
|
|
@ -67,7 +67,6 @@ export default class EventIndex extends EventEmitter {
|
||||||
|
|
||||||
client.on('sync', this.onSync);
|
client.on('sync', this.onSync);
|
||||||
client.on('Room.timeline', this.onRoomTimeline);
|
client.on('Room.timeline', this.onRoomTimeline);
|
||||||
client.on('Event.decrypted', this.onEventDecrypted);
|
|
||||||
client.on('Room.timelineReset', this.onTimelineReset);
|
client.on('Room.timelineReset', this.onTimelineReset);
|
||||||
client.on('Room.redaction', this.onRedaction);
|
client.on('Room.redaction', this.onRedaction);
|
||||||
client.on('RoomState.events', this.onRoomStateEvent);
|
client.on('RoomState.events', this.onRoomStateEvent);
|
||||||
|
@ -82,7 +81,6 @@ export default class EventIndex extends EventEmitter {
|
||||||
|
|
||||||
client.removeListener('sync', this.onSync);
|
client.removeListener('sync', this.onSync);
|
||||||
client.removeListener('Room.timeline', this.onRoomTimeline);
|
client.removeListener('Room.timeline', this.onRoomTimeline);
|
||||||
client.removeListener('Event.decrypted', this.onEventDecrypted);
|
|
||||||
client.removeListener('Room.timelineReset', this.onTimelineReset);
|
client.removeListener('Room.timelineReset', this.onTimelineReset);
|
||||||
client.removeListener('Room.redaction', this.onRedaction);
|
client.removeListener('Room.redaction', this.onRedaction);
|
||||||
client.removeListener('RoomState.events', this.onRoomStateEvent);
|
client.removeListener('RoomState.events', this.onRoomStateEvent);
|
||||||
|
@ -221,18 +219,6 @@ export default class EventIndex extends EventEmitter {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* The Event.decrypted listener.
|
|
||||||
*
|
|
||||||
* Checks if the event was marked for addition in the Room.timeline
|
|
||||||
* listener, if so queues it up to be added to the index.
|
|
||||||
*/
|
|
||||||
private onEventDecrypted = async (ev: MatrixEvent, err: Error) => {
|
|
||||||
// If the event isn't in our live event set, ignore it.
|
|
||||||
if (err) return;
|
|
||||||
await this.addLiveEventToIndex(ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Room.redaction listener.
|
* The Room.redaction listener.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -132,8 +132,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// Update any settings here, as some may have happened before we were logically ready.
|
// Update any settings here, as some may have happened before we were logically ready.
|
||||||
console.log("Regenerating room lists: Startup");
|
console.log("Regenerating room lists: Startup");
|
||||||
await this.readAndCacheSettingsFromStore();
|
await this.readAndCacheSettingsFromStore();
|
||||||
await this.regenerateAllLists({ trigger: false });
|
this.regenerateAllLists({ trigger: false });
|
||||||
await this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed
|
this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed
|
||||||
|
|
||||||
this.updateFn.mark(); // we almost certainly want to trigger an update.
|
this.updateFn.mark(); // we almost certainly want to trigger an update.
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
|
@ -150,7 +150,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
await this.updateState({
|
await this.updateState({
|
||||||
tagsEnabled,
|
tagsEnabled,
|
||||||
});
|
});
|
||||||
await this.updateAlgorithmInstances();
|
this.updateAlgorithmInstances();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,23 +158,23 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
* @param trigger Set to false to prevent a list update from being sent. Should only
|
* @param trigger Set to false to prevent a list update from being sent. Should only
|
||||||
* be used if the calling code will manually trigger the update.
|
* be used if the calling code will manually trigger the update.
|
||||||
*/
|
*/
|
||||||
private async handleRVSUpdate({ trigger = true }) {
|
private handleRVSUpdate({ trigger = true }) {
|
||||||
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
|
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
|
||||||
|
|
||||||
const activeRoomId = RoomViewStore.getRoomId();
|
const activeRoomId = RoomViewStore.getRoomId();
|
||||||
if (!activeRoomId && this.algorithm.stickyRoom) {
|
if (!activeRoomId && this.algorithm.stickyRoom) {
|
||||||
await this.algorithm.setStickyRoom(null);
|
this.algorithm.setStickyRoom(null);
|
||||||
} else if (activeRoomId) {
|
} else if (activeRoomId) {
|
||||||
const activeRoom = this.matrixClient.getRoom(activeRoomId);
|
const activeRoom = this.matrixClient.getRoom(activeRoomId);
|
||||||
if (!activeRoom) {
|
if (!activeRoom) {
|
||||||
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
||||||
await this.algorithm.setStickyRoom(null);
|
this.algorithm.setStickyRoom(null);
|
||||||
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log(`Changing sticky room to ${activeRoomId}`);
|
console.log(`Changing sticky room to ${activeRoomId}`);
|
||||||
}
|
}
|
||||||
await this.algorithm.setStickyRoom(activeRoom);
|
this.algorithm.setStickyRoom(activeRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
console.log("Regenerating room lists: Settings changed");
|
console.log("Regenerating room lists: Settings changed");
|
||||||
await this.readAndCacheSettingsFromStore();
|
await this.readAndCacheSettingsFromStore();
|
||||||
|
|
||||||
await this.regenerateAllLists({ trigger: false }); // regenerate the lists now
|
this.regenerateAllLists({ trigger: false }); // regenerate the lists now
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -368,7 +368,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
|
console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
|
||||||
}
|
}
|
||||||
await this.algorithm.setStickyRoom(null);
|
this.algorithm.setStickyRoom(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
||||||
|
@ -377,7 +377,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log(`[RoomListDebug] Removing previous room from room list`);
|
console.log(`[RoomListDebug] Removing previous room from room list`);
|
||||||
}
|
}
|
||||||
await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,7 +433,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return; // don't do anything on new/moved rooms which ought not to be shown
|
return; // don't do anything on new/moved rooms which ought not to be shown
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
const shouldUpdate = this.algorithm.handleRoomUpdate(room, cause);
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
|
@ -462,13 +462,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
// Reset the sticky room before resetting the known rooms so the algorithm
|
// Reset the sticky room before resetting the known rooms so the algorithm
|
||||||
// doesn't freak out.
|
// doesn't freak out.
|
||||||
await this.algorithm.setStickyRoom(null);
|
this.algorithm.setStickyRoom(null);
|
||||||
await this.algorithm.setKnownRooms(rooms);
|
this.algorithm.setKnownRooms(rooms);
|
||||||
|
|
||||||
// Set the sticky room back, if needed, now that we have updated the store.
|
// Set the sticky room back, if needed, now that we have updated the store.
|
||||||
// This will use relative stickyness to the new room set.
|
// This will use relative stickyness to the new room set.
|
||||||
if (stickyIsStillPresent) {
|
if (stickyIsStillPresent) {
|
||||||
await this.algorithm.setStickyRoom(currentSticky);
|
this.algorithm.setStickyRoom(currentSticky);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, mark an update and resume updates from the algorithm
|
// Finally, mark an update and resume updates from the algorithm
|
||||||
|
@ -477,12 +477,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||||
await this.setAndPersistTagSorting(tagId, sort);
|
this.setAndPersistTagSorting(tagId, sort);
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
private setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||||
await this.algorithm.setTagSorting(tagId, sort);
|
this.algorithm.setTagSorting(tagId, sort);
|
||||||
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
||||||
localStorage.setItem(`mx_tagSort_${tagId}`, sort);
|
localStorage.setItem(`mx_tagSort_${tagId}`, sort);
|
||||||
}
|
}
|
||||||
|
@ -520,13 +520,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return tagSort;
|
return tagSort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setListOrder(tagId: TagID, order: ListAlgorithm) {
|
public setListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||||
await this.setAndPersistListOrder(tagId, order);
|
this.setAndPersistListOrder(tagId, order);
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) {
|
private setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||||
await this.algorithm.setListOrdering(tagId, order);
|
this.algorithm.setListOrdering(tagId, order);
|
||||||
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
||||||
localStorage.setItem(`mx_listOrder_${tagId}`, order);
|
localStorage.setItem(`mx_listOrder_${tagId}`, order);
|
||||||
}
|
}
|
||||||
|
@ -563,7 +563,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
return listOrder;
|
return listOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAlgorithmInstances() {
|
private updateAlgorithmInstances() {
|
||||||
// We'll require an update, so mark for one. Marking now also prevents the calls
|
// We'll require an update, so mark for one. Marking now also prevents the calls
|
||||||
// to setTagSorting and setListOrder from causing triggers.
|
// to setTagSorting and setListOrder from causing triggers.
|
||||||
this.updateFn.mark();
|
this.updateFn.mark();
|
||||||
|
@ -576,10 +576,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const listOrder = this.calculateListOrder(tag);
|
const listOrder = this.calculateListOrder(tag);
|
||||||
|
|
||||||
if (tagSort !== definedSort) {
|
if (tagSort !== definedSort) {
|
||||||
await this.setAndPersistTagSorting(tag, tagSort);
|
this.setAndPersistTagSorting(tag, tagSort);
|
||||||
}
|
}
|
||||||
if (listOrder !== definedOrder) {
|
if (listOrder !== definedOrder) {
|
||||||
await this.setAndPersistListOrder(tag, listOrder);
|
this.setAndPersistListOrder(tag, listOrder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -632,7 +632,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
* @param trigger Set to false to prevent a list update from being sent. Should only
|
* @param trigger Set to false to prevent a list update from being sent. Should only
|
||||||
* be used if the calling code will manually trigger the update.
|
* be used if the calling code will manually trigger the update.
|
||||||
*/
|
*/
|
||||||
public async regenerateAllLists({ trigger = true }) {
|
public regenerateAllLists({ trigger = true }) {
|
||||||
console.warn("Regenerating all room lists");
|
console.warn("Regenerating all room lists");
|
||||||
|
|
||||||
const rooms = this.getPlausibleRooms();
|
const rooms = this.getPlausibleRooms();
|
||||||
|
@ -656,8 +656,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
|
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.algorithm.populateTags(sorts, orders);
|
this.algorithm.populateTags(sorts, orders);
|
||||||
await this.algorithm.setKnownRooms(rooms);
|
this.algorithm.setKnownRooms(rooms);
|
||||||
|
|
||||||
this.initialListsGenerated = true;
|
this.initialListsGenerated = true;
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
|
import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
|
||||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||||
import {
|
import {
|
||||||
|
@ -122,8 +123,8 @@ export class Algorithm extends EventEmitter {
|
||||||
* Awaitable version of the sticky room setter.
|
* Awaitable version of the sticky room setter.
|
||||||
* @param val The new room to sticky.
|
* @param val The new room to sticky.
|
||||||
*/
|
*/
|
||||||
public async setStickyRoom(val: Room) {
|
public setStickyRoom(val: Room) {
|
||||||
await this.updateStickyRoom(val);
|
this.updateStickyRoom(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTagSorting(tagId: TagID): SortAlgorithm {
|
public getTagSorting(tagId: TagID): SortAlgorithm {
|
||||||
|
@ -131,13 +132,13 @@ export class Algorithm extends EventEmitter {
|
||||||
return this.sortAlgorithms[tagId];
|
return this.sortAlgorithms[tagId];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
public setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||||
if (!tagId) throw new Error("Tag ID must be defined");
|
if (!tagId) throw new Error("Tag ID must be defined");
|
||||||
if (!sort) throw new Error("Algorithm must be defined");
|
if (!sort) throw new Error("Algorithm must be defined");
|
||||||
this.sortAlgorithms[tagId] = sort;
|
this.sortAlgorithms[tagId] = sort;
|
||||||
|
|
||||||
const algorithm: OrderingAlgorithm = this.algorithms[tagId];
|
const algorithm: OrderingAlgorithm = this.algorithms[tagId];
|
||||||
await algorithm.setSortAlgorithm(sort);
|
algorithm.setSortAlgorithm(sort);
|
||||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||||
|
@ -148,7 +149,7 @@ export class Algorithm extends EventEmitter {
|
||||||
return this.listAlgorithms[tagId];
|
return this.listAlgorithms[tagId];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setListOrdering(tagId: TagID, order: ListAlgorithm) {
|
public setListOrdering(tagId: TagID, order: ListAlgorithm) {
|
||||||
if (!tagId) throw new Error("Tag ID must be defined");
|
if (!tagId) throw new Error("Tag ID must be defined");
|
||||||
if (!order) throw new Error("Algorithm must be defined");
|
if (!order) throw new Error("Algorithm must be defined");
|
||||||
this.listAlgorithms[tagId] = order;
|
this.listAlgorithms[tagId] = order;
|
||||||
|
@ -156,7 +157,7 @@ export class Algorithm extends EventEmitter {
|
||||||
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
|
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
|
||||||
this.algorithms[tagId] = algorithm;
|
this.algorithms[tagId] = algorithm;
|
||||||
|
|
||||||
await algorithm.setRooms(this._cachedRooms[tagId]);
|
algorithm.setRooms(this._cachedRooms[tagId]);
|
||||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||||
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed
|
||||||
|
@ -183,31 +184,25 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleFilterChange() {
|
private handleFilterChange() {
|
||||||
await this.recalculateFilteredRooms();
|
this.recalculateFilteredRooms();
|
||||||
|
|
||||||
// re-emit the update so the list store can fire an off-cycle update if needed
|
// re-emit the update so the list store can fire an off-cycle update if needed
|
||||||
if (this.updatesInhibited) return;
|
if (this.updatesInhibited) return;
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateStickyRoom(val: Room) {
|
private updateStickyRoom(val: Room) {
|
||||||
try {
|
this.doUpdateStickyRoom(val);
|
||||||
return await this.doUpdateStickyRoom(val);
|
this._lastStickyRoom = null; // clear to indicate we're done changing
|
||||||
} finally {
|
|
||||||
this._lastStickyRoom = null; // clear to indicate we're done changing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doUpdateStickyRoom(val: Room) {
|
private doUpdateStickyRoom(val: Room) {
|
||||||
if (SpaceStore.spacesEnabled && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
|
if (SpaceStore.spacesEnabled && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
|
||||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||||
val = null;
|
val = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
|
||||||
// otherwise we risk duplicating rooms.
|
|
||||||
|
|
||||||
if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
|
if (val && !VisibilityProvider.instance.isRoomVisible(val)) {
|
||||||
val = null; // the room isn't visible - lie to the rest of this function
|
val = null; // the room isn't visible - lie to the rest of this function
|
||||||
}
|
}
|
||||||
|
@ -223,7 +218,7 @@ export class Algorithm extends EventEmitter {
|
||||||
this._stickyRoom = null; // clear before we go to update the algorithm
|
this._stickyRoom = null; // clear before we go to update the algorithm
|
||||||
|
|
||||||
// Lie to the algorithm and re-add the room to the algorithm
|
// Lie to the algorithm and re-add the room to the algorithm
|
||||||
await this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom);
|
this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -269,10 +264,10 @@ export class Algorithm extends EventEmitter {
|
||||||
// referential checks as the references can differ through the lifecycle.
|
// referential checks as the references can differ through the lifecycle.
|
||||||
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
|
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
|
||||||
// Lie to the algorithm and re-add the room to the algorithm
|
// Lie to the algorithm and re-add the room to the algorithm
|
||||||
await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom);
|
||||||
}
|
}
|
||||||
// Lie to the algorithm and remove the room from it's field of view
|
// Lie to the algorithm and remove the room from it's field of view
|
||||||
await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved);
|
||||||
|
|
||||||
// Check for tag & position changes while we're here. We also check the room to ensure
|
// Check for tag & position changes while we're here. We also check the room to ensure
|
||||||
// it is still the same room.
|
// it is still the same room.
|
||||||
|
@ -462,9 +457,8 @@ export class Algorithm extends EventEmitter {
|
||||||
* them.
|
* them.
|
||||||
* @param {ITagSortingMap} tagSortingMap The tags to generate.
|
* @param {ITagSortingMap} tagSortingMap The tags to generate.
|
||||||
* @param {IListOrderingMap} listOrderingMap The ordering of those tags.
|
* @param {IListOrderingMap} listOrderingMap The ordering of those tags.
|
||||||
* @returns {Promise<*>} A promise which resolves when complete.
|
|
||||||
*/
|
*/
|
||||||
public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise<any> {
|
public populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): void {
|
||||||
if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
|
if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`);
|
||||||
if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
|
if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
|
||||||
if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
|
if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) {
|
||||||
|
@ -513,9 +507,8 @@ export class Algorithm extends EventEmitter {
|
||||||
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
|
* Seeds the Algorithm with a set of rooms. The algorithm will discard all
|
||||||
* previously known information and instead use these rooms instead.
|
* previously known information and instead use these rooms instead.
|
||||||
* @param {Room[]} rooms The rooms to force the algorithm to use.
|
* @param {Room[]} rooms The rooms to force the algorithm to use.
|
||||||
* @returns {Promise<*>} A promise which resolves when complete.
|
|
||||||
*/
|
*/
|
||||||
public async setKnownRooms(rooms: Room[]): Promise<any> {
|
public setKnownRooms(rooms: Room[]): void {
|
||||||
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
|
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
|
||||||
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
|
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
|
||||||
|
|
||||||
|
@ -529,7 +522,7 @@ export class Algorithm extends EventEmitter {
|
||||||
// Before we go any further we need to clear (but remember) the sticky room to
|
// Before we go any further we need to clear (but remember) the sticky room to
|
||||||
// avoid accidentally duplicating it in the list.
|
// avoid accidentally duplicating it in the list.
|
||||||
const oldStickyRoom = this._stickyRoom;
|
const oldStickyRoom = this._stickyRoom;
|
||||||
await this.updateStickyRoom(null);
|
if (oldStickyRoom) this.updateStickyRoom(null);
|
||||||
|
|
||||||
this.rooms = rooms;
|
this.rooms = rooms;
|
||||||
|
|
||||||
|
@ -541,7 +534,7 @@ export class Algorithm extends EventEmitter {
|
||||||
|
|
||||||
// If we can avoid doing work, do so.
|
// If we can avoid doing work, do so.
|
||||||
if (!rooms.length) {
|
if (!rooms.length) {
|
||||||
await this.generateFreshTags(newTags); // just in case it wants to do something
|
this.generateFreshTags(newTags); // just in case it wants to do something
|
||||||
this.cachedRooms = newTags;
|
this.cachedRooms = newTags;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -578,7 +571,7 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.generateFreshTags(newTags);
|
this.generateFreshTags(newTags);
|
||||||
|
|
||||||
this.cachedRooms = newTags; // this recalculates the filtered rooms for us
|
this.cachedRooms = newTags; // this recalculates the filtered rooms for us
|
||||||
this.updateTagsFromCache();
|
this.updateTagsFromCache();
|
||||||
|
@ -587,7 +580,7 @@ export class Algorithm extends EventEmitter {
|
||||||
// it was. It's entirely possible that it changed lists though, so if it did then
|
// it was. It's entirely possible that it changed lists though, so if it did then
|
||||||
// we also have to update the position of it.
|
// we also have to update the position of it.
|
||||||
if (oldStickyRoom && oldStickyRoom.room) {
|
if (oldStickyRoom && oldStickyRoom.room) {
|
||||||
await this.updateStickyRoom(oldStickyRoom.room);
|
this.updateStickyRoom(oldStickyRoom.room);
|
||||||
if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan
|
if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan
|
||||||
if (this._stickyRoom.tag !== oldStickyRoom.tag) {
|
if (this._stickyRoom.tag !== oldStickyRoom.tag) {
|
||||||
// We put the sticky room at the top of the list to treat it as an obvious tag change.
|
// We put the sticky room at the top of the list to treat it as an obvious tag change.
|
||||||
|
@ -652,16 +645,15 @@ export class Algorithm extends EventEmitter {
|
||||||
* @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
|
* @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
|
||||||
* will already have the rooms which belong to it - they just need ordering. Must
|
* will already have the rooms which belong to it - they just need ordering. Must
|
||||||
* be mutated in place.
|
* be mutated in place.
|
||||||
* @returns {Promise<*>} A promise which resolves when complete.
|
|
||||||
*/
|
*/
|
||||||
private async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
|
private generateFreshTags(updatedTagMap: ITagMap): void {
|
||||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||||
|
|
||||||
for (const tag of Object.keys(updatedTagMap)) {
|
for (const tag of Object.keys(updatedTagMap)) {
|
||||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||||
|
|
||||||
await algorithm.setRooms(updatedTagMap[tag]);
|
algorithm.setRooms(updatedTagMap[tag]);
|
||||||
updatedTagMap[tag] = algorithm.orderedRooms;
|
updatedTagMap[tag] = algorithm.orderedRooms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -673,11 +665,10 @@ export class Algorithm extends EventEmitter {
|
||||||
* may no-op this request if no changes are required.
|
* may no-op this request if no changes are required.
|
||||||
* @param {Room} room The room which might have affected sorting.
|
* @param {Room} room The room which might have affected sorting.
|
||||||
* @param {RoomUpdateCause} cause The reason for the update being triggered.
|
* @param {RoomUpdateCause} cause The reason for the update being triggered.
|
||||||
* @returns {Promise<boolean>} A promise which resolve to true or false
|
* @returns {Promise<boolean>} A boolean of whether or not getOrderedRooms()
|
||||||
* depending on whether or not getOrderedRooms() should be called after
|
* should be called after processing.
|
||||||
* processing.
|
|
||||||
*/
|
*/
|
||||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
|
||||||
|
@ -685,9 +676,9 @@ export class Algorithm extends EventEmitter {
|
||||||
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
|
||||||
|
|
||||||
// Note: check the isSticky against the room ID just in case the reference is wrong
|
// Note: check the isSticky against the room ID just in case the reference is wrong
|
||||||
const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId;
|
const isSticky = this._stickyRoom?.room?.roomId === room.roomId;
|
||||||
if (cause === RoomUpdateCause.NewRoom) {
|
if (cause === RoomUpdateCause.NewRoom) {
|
||||||
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
const isForLastSticky = this._lastStickyRoom?.room === room;
|
||||||
const roomTags = this.roomIdsToTags[room.roomId];
|
const roomTags = this.roomIdsToTags[room.roomId];
|
||||||
const hasTags = roomTags && roomTags.length > 0;
|
const hasTags = roomTags && roomTags.length > 0;
|
||||||
|
|
||||||
|
@ -744,7 +735,7 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
|
||||||
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
|
||||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
|
||||||
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
this._cachedRooms[rmTag] = algorithm.orderedRooms;
|
||||||
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
||||||
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed
|
||||||
|
@ -756,7 +747,7 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
const algorithm: OrderingAlgorithm = this.algorithms[addTag];
|
||||||
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
|
||||||
await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
|
||||||
this._cachedRooms[addTag] = algorithm.orderedRooms;
|
this._cachedRooms[addTag] = algorithm.orderedRooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -789,7 +780,7 @@ export class Algorithm extends EventEmitter {
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// We have to clear the lock as the sticky room change will trigger updates.
|
// We have to clear the lock as the sticky room change will trigger updates.
|
||||||
await this.setStickyRoom(room);
|
this.setStickyRoom(room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -852,7 +843,7 @@ export class Algorithm extends EventEmitter {
|
||||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||||
|
|
||||||
await algorithm.handleRoomUpdate(room, cause);
|
algorithm.handleRoomUpdate(room, cause);
|
||||||
this._cachedRooms[tag] = algorithm.orderedRooms;
|
this._cachedRooms[tag] = algorithm.orderedRooms;
|
||||||
|
|
||||||
// Flag that we've done something
|
// Flag that we've done something
|
||||||
|
|
|
@ -94,15 +94,15 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
return state.color;
|
return state.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRooms(rooms: Room[]): Promise<any> {
|
public setRooms(rooms: Room[]): void {
|
||||||
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
||||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||||
} else {
|
} else {
|
||||||
// Every other sorting type affects the categories, not the whole tag.
|
// Every other sorting type affects the categories, not the whole tag.
|
||||||
const categorized = this.categorizeRooms(rooms);
|
const categorized = this.categorizeRooms(rooms);
|
||||||
for (const category of Object.keys(categorized)) {
|
for (const category of Object.keys(categorized)) {
|
||||||
const roomsToOrder = categorized[category];
|
const roomsToOrder = categorized[category];
|
||||||
categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
|
categorized[category] = sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newlyOrganized: Room[] = [];
|
const newlyOrganized: Room[] = [];
|
||||||
|
@ -118,12 +118,12 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSplice(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
private handleSplice(room: Room, cause: RoomUpdateCause): boolean {
|
||||||
if (cause === RoomUpdateCause.NewRoom) {
|
if (cause === RoomUpdateCause.NewRoom) {
|
||||||
const category = this.getRoomCategory(room);
|
const category = this.getRoomCategory(room);
|
||||||
this.alterCategoryPositionBy(category, 1, this.indices);
|
this.alterCategoryPositionBy(category, 1, this.indices);
|
||||||
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||||
await this.sortCategory(category);
|
this.sortCategory(category);
|
||||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||||
const roomIdx = this.getRoomIndex(room);
|
const roomIdx = this.getRoomIndex(room);
|
||||||
if (roomIdx === -1) {
|
if (roomIdx === -1) {
|
||||||
|
@ -141,55 +141,49 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
|
||||||
try {
|
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
|
||||||
await this.updateLock.acquireAsync();
|
return this.handleSplice(room, cause);
|
||||||
|
|
||||||
if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) {
|
|
||||||
return this.handleSplice(room, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
|
|
||||||
throw new Error(`Unsupported update cause: ${cause}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const category = this.getRoomCategory(room);
|
|
||||||
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
|
||||||
return; // Nothing to do here.
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomIdx = this.getRoomIndex(room);
|
|
||||||
if (roomIdx === -1) {
|
|
||||||
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to avoid doing array operations if we don't have to: only move rooms within
|
|
||||||
// the categories if we're jumping categories
|
|
||||||
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
|
|
||||||
if (oldCategory !== category) {
|
|
||||||
// Move the room and update the indices
|
|
||||||
this.moveRoomIndexes(1, oldCategory, category, this.indices);
|
|
||||||
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
|
|
||||||
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
|
||||||
// Note: if moveRoomIndexes() is called after the splice then the insert operation
|
|
||||||
// will happen in the wrong place. Because we would have already adjusted the index
|
|
||||||
// for the category, we don't need to determine how the room is moving in the list.
|
|
||||||
// If we instead tried to insert before updating the indices, we'd have to determine
|
|
||||||
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
|
|
||||||
// current position, as it'll affect the category's start index after we remove the
|
|
||||||
// room from the array.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the category now that we've dumped the room in
|
|
||||||
await this.sortCategory(category);
|
|
||||||
|
|
||||||
return true; // change made
|
|
||||||
} finally {
|
|
||||||
await this.updateLock.release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) {
|
||||||
|
throw new Error(`Unsupported update cause: ${cause}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = this.getRoomCategory(room);
|
||||||
|
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
||||||
|
return; // Nothing to do here.
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomIdx = this.getRoomIndex(room);
|
||||||
|
if (roomIdx === -1) {
|
||||||
|
throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to avoid doing array operations if we don't have to: only move rooms within
|
||||||
|
// the categories if we're jumping categories
|
||||||
|
const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices);
|
||||||
|
if (oldCategory !== category) {
|
||||||
|
// Move the room and update the indices
|
||||||
|
this.moveRoomIndexes(1, oldCategory, category, this.indices);
|
||||||
|
this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
|
||||||
|
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||||
|
// Note: if moveRoomIndexes() is called after the splice then the insert operation
|
||||||
|
// will happen in the wrong place. Because we would have already adjusted the index
|
||||||
|
// for the category, we don't need to determine how the room is moving in the list.
|
||||||
|
// If we instead tried to insert before updating the indices, we'd have to determine
|
||||||
|
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
|
||||||
|
// current position, as it'll affect the category's start index after we remove the
|
||||||
|
// room from the array.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the category now that we've dumped the room in
|
||||||
|
this.sortCategory(category);
|
||||||
|
|
||||||
|
return true; // change made
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sortCategory(category: NotificationColor) {
|
private sortCategory(category: NotificationColor) {
|
||||||
// This should be relatively quick because the room is usually inserted at the top of the
|
// This should be relatively quick because the room is usually inserted at the top of the
|
||||||
// category, and most popular sorting algorithms will deal with trying to keep the active
|
// category, and most popular sorting algorithms will deal with trying to keep the active
|
||||||
// room at the top/start of the category. For the few algorithms that will have to move the
|
// room at the top/start of the category. For the few algorithms that will have to move the
|
||||||
|
@ -201,7 +195,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
const startIdx = this.indices[category];
|
const startIdx = this.indices[category];
|
||||||
const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine
|
const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine
|
||||||
const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort);
|
const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort);
|
||||||
const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm);
|
const sorted = sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm);
|
||||||
this.cachedOrderedRooms.splice(startIdx, 0, ...sorted);
|
this.cachedOrderedRooms.splice(startIdx, 0, ...sorted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,42 +29,32 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
|
||||||
super(tagId, initialSortingAlgorithm);
|
super(tagId, initialSortingAlgorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRooms(rooms: Room[]): Promise<any> {
|
public setRooms(rooms: Room[]): void {
|
||||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRoomUpdate(room, cause): Promise<boolean> {
|
public handleRoomUpdate(room, cause): boolean {
|
||||||
try {
|
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
|
||||||
await this.updateLock.acquireAsync();
|
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
|
||||||
|
if (!isSplice && !isInPlace) {
|
||||||
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
|
throw new Error(`Unsupported update cause: ${cause}`);
|
||||||
const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt;
|
|
||||||
if (!isSplice && !isInPlace) {
|
|
||||||
throw new Error(`Unsupported update cause: ${cause}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cause === RoomUpdateCause.NewRoom) {
|
|
||||||
this.cachedOrderedRooms.push(room);
|
|
||||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
|
||||||
const idx = this.getRoomIndex(room);
|
|
||||||
if (idx >= 0) {
|
|
||||||
this.cachedOrderedRooms.splice(idx, 1);
|
|
||||||
} else {
|
|
||||||
console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457
|
|
||||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
|
||||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(
|
|
||||||
this.cachedOrderedRooms,
|
|
||||||
this.tagId,
|
|
||||||
this.sortingAlgorithm,
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} finally {
|
|
||||||
await this.updateLock.release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cause === RoomUpdateCause.NewRoom) {
|
||||||
|
this.cachedOrderedRooms.push(room);
|
||||||
|
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||||
|
const idx = this.getRoomIndex(room);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.cachedOrderedRooms.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457
|
||||||
|
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
||||||
|
this.cachedOrderedRooms = sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomUpdateCause, TagID } from "../../models";
|
import { RoomUpdateCause, TagID } from "../../models";
|
||||||
import { SortAlgorithm } from "../models";
|
import { SortAlgorithm } from "../models";
|
||||||
import AwaitLock from "await-lock";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a list ordering algorithm. Subclasses should populate the
|
* Represents a list ordering algorithm. Subclasses should populate the
|
||||||
|
@ -26,7 +25,6 @@ import AwaitLock from "await-lock";
|
||||||
export abstract class OrderingAlgorithm {
|
export abstract class OrderingAlgorithm {
|
||||||
protected cachedOrderedRooms: Room[];
|
protected cachedOrderedRooms: Room[];
|
||||||
protected sortingAlgorithm: SortAlgorithm;
|
protected sortingAlgorithm: SortAlgorithm;
|
||||||
protected readonly updateLock = new AwaitLock();
|
|
||||||
|
|
||||||
protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
@ -45,21 +43,20 @@ export abstract class OrderingAlgorithm {
|
||||||
* @param newAlgorithm The new algorithm. Must be defined.
|
* @param newAlgorithm The new algorithm. Must be defined.
|
||||||
* @returns Resolves when complete.
|
* @returns Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public async setSortAlgorithm(newAlgorithm: SortAlgorithm) {
|
public setSortAlgorithm(newAlgorithm: SortAlgorithm) {
|
||||||
if (!newAlgorithm) throw new Error("A sorting algorithm must be defined");
|
if (!newAlgorithm) throw new Error("A sorting algorithm must be defined");
|
||||||
this.sortingAlgorithm = newAlgorithm;
|
this.sortingAlgorithm = newAlgorithm;
|
||||||
|
|
||||||
// Force regeneration of the rooms
|
// Force regeneration of the rooms
|
||||||
await this.setRooms(this.orderedRooms);
|
this.setRooms(this.orderedRooms);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the rooms the algorithm should be handling, implying a reconstruction
|
* Sets the rooms the algorithm should be handling, implying a reconstruction
|
||||||
* of the ordering.
|
* of the ordering.
|
||||||
* @param rooms The rooms to use going forward.
|
* @param rooms The rooms to use going forward.
|
||||||
* @returns Resolves when complete.
|
|
||||||
*/
|
*/
|
||||||
public abstract setRooms(rooms: Room[]): Promise<any>;
|
public abstract setRooms(rooms: Room[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a room update. The Algorithm will only call this for causes which
|
* Handle a room update. The Algorithm will only call this for causes which
|
||||||
|
@ -69,7 +66,7 @@ export abstract class OrderingAlgorithm {
|
||||||
* @param cause The cause of the update.
|
* @param cause The cause of the update.
|
||||||
* @returns True if the update requires the Algorithm to update the presentation layers.
|
* @returns True if the update requires the Algorithm to update the presentation layers.
|
||||||
*/
|
*/
|
||||||
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>;
|
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean;
|
||||||
|
|
||||||
protected getRoomIndex(room: Room): number {
|
protected getRoomIndex(room: Room): number {
|
||||||
let roomIdx = this.cachedOrderedRooms.indexOf(room);
|
let roomIdx = this.cachedOrderedRooms.indexOf(room);
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { compare } from "../../../../utils/strings";
|
||||||
* Sorts rooms according to the browser's determination of alphabetic.
|
* Sorts rooms according to the browser's determination of alphabetic.
|
||||||
*/
|
*/
|
||||||
export class AlphabeticAlgorithm implements IAlgorithm {
|
export class AlphabeticAlgorithm implements IAlgorithm {
|
||||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||||
return rooms.sort((a, b) => {
|
return rooms.sort((a, b) => {
|
||||||
return compare(a.name, b.name);
|
return compare(a.name, b.name);
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ export interface IAlgorithm {
|
||||||
* Sorts the given rooms according to the sorting rules of the algorithm.
|
* Sorts the given rooms according to the sorting rules of the algorithm.
|
||||||
* @param {Room[]} rooms The rooms to sort.
|
* @param {Room[]} rooms The rooms to sort.
|
||||||
* @param {TagID} tagId The tag ID in which the rooms are being sorted.
|
* @param {TagID} tagId The tag ID in which the rooms are being sorted.
|
||||||
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
|
* @returns {Room[]} Returns the sorted rooms.
|
||||||
*/
|
*/
|
||||||
sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]>;
|
sortRooms(rooms: Room[], tagId: TagID): Room[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { IAlgorithm } from "./IAlgorithm";
|
||||||
* Sorts rooms according to the tag's `order` property on the room.
|
* Sorts rooms according to the tag's `order` property on the room.
|
||||||
*/
|
*/
|
||||||
export class ManualAlgorithm implements IAlgorithm {
|
export class ManualAlgorithm implements IAlgorithm {
|
||||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||||
const getOrderProp = (r: Room) => r.tags[tagId].order || 0;
|
const getOrderProp = (r: Room) => r.tags[tagId].order || 0;
|
||||||
return rooms.sort((a, b) => {
|
return rooms.sort((a, b) => {
|
||||||
return getOrderProp(a) - getOrderProp(b);
|
return getOrderProp(a) - getOrderProp(b);
|
||||||
|
|
|
@ -97,7 +97,7 @@ export const sortRooms = (rooms: Room[]): Room[] => {
|
||||||
* useful to the user.
|
* useful to the user.
|
||||||
*/
|
*/
|
||||||
export class RecentAlgorithm implements IAlgorithm {
|
export class RecentAlgorithm implements IAlgorithm {
|
||||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||||
return sortRooms(rooms);
|
return sortRooms(rooms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,8 +46,8 @@ export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorith
|
||||||
* @param {Room[]} rooms The rooms to sort.
|
* @param {Room[]} rooms The rooms to sort.
|
||||||
* @param {TagID} tagId The tag in which the sorting is occurring.
|
* @param {TagID} tagId The tag in which the sorting is occurring.
|
||||||
* @param {SortAlgorithm} algorithm The algorithm to use for sorting.
|
* @param {SortAlgorithm} algorithm The algorithm to use for sorting.
|
||||||
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
|
* @returns {Room[]} Returns the sorted rooms.
|
||||||
*/
|
*/
|
||||||
export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise<Room[]> {
|
export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Room[] {
|
||||||
return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId);
|
return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import url from 'url';
|
|
||||||
import qs from 'qs';
|
|
||||||
|
|
||||||
import SdkConfig from '../SdkConfig';
|
import SdkConfig from '../SdkConfig';
|
||||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
|
||||||
|
@ -28,11 +25,8 @@ export function getHostingLink(campaign) {
|
||||||
if (MatrixClientPeg.get().getDomain() !== 'matrix.org') return null;
|
if (MatrixClientPeg.get().getDomain() !== 'matrix.org') return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hostingUrl = url.parse(hostingLink);
|
const hostingUrl = new URL(hostingLink);
|
||||||
const params = qs.parse(hostingUrl.query);
|
hostingUrl.searchParams.set("utm_campaign", campaign);
|
||||||
params.utm_campaign = campaign;
|
|
||||||
hostingUrl.search = undefined;
|
|
||||||
hostingUrl.query = params;
|
|
||||||
return hostingUrl.format();
|
return hostingUrl.format();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return hostingLink;
|
return hostingLink;
|
||||||
|
|
|
@ -96,6 +96,7 @@ export function createTestClient() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
decryptEventIfNeeded: () => Promise.resolve(),
|
decryptEventIfNeeded: () => Promise.resolve(),
|
||||||
|
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue