Merge remote-tracking branch 'upstream/develop' into task/cleanup-replies
commit
75fc1299fb
|
@ -149,6 +149,7 @@
|
|||
@import "./views/elements/_StyledCheckbox.scss";
|
||||
@import "./views/elements/_StyledRadioButton.scss";
|
||||
@import "./views/elements/_SyntaxHighlight.scss";
|
||||
@import "./views/elements/_TagComposer.scss";
|
||||
@import "./views/elements/_TextWithTooltip.scss";
|
||||
@import "./views/elements/_ToggleSwitch.scss";
|
||||
@import "./views/elements/_Tooltip.scss";
|
||||
|
@ -263,9 +264,9 @@
|
|||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
@import "./views/verification/_VerificationShowSas.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallPreview.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_CallViewForRoom.scss";
|
||||
@import "./views/voip/_CallPreview.scss";
|
||||
@import "./views/voip/_DialPad.scss";
|
||||
@import "./views/voip/_DialPadContextMenu.scss";
|
||||
@import "./views/voip/_DialPadModal.scss";
|
||||
|
|
|
@ -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 {
|
||||
margin: 0;
|
||||
.mx_SenderProfile {
|
||||
order: unset;
|
||||
max-width: unset;
|
||||
width: unset;
|
||||
max-width: var(--name-width);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
|
|
@ -193,6 +193,10 @@ limitations under the License.
|
|||
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 {
|
||||
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");
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_UserNotifSettings_tableRow {
|
||||
display: table-row;
|
||||
}
|
||||
.mx_UserNotifSettings {
|
||||
color: $primary-fg-color; // override from default settings page styles
|
||||
|
||||
.mx_UserNotifSettings_inputCell {
|
||||
display: table-cell;
|
||||
padding-bottom: 8px;
|
||||
padding-right: 8px;
|
||||
width: 16px;
|
||||
}
|
||||
.mx_UserNotifSettings_pushRulesTable {
|
||||
width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
margin-top: 40px;
|
||||
|
||||
.mx_UserNotifSettings_labelCell {
|
||||
padding-bottom: 8px;
|
||||
width: 400px;
|
||||
display: table-cell;
|
||||
}
|
||||
tr > th {
|
||||
font-weight: $font-semi-bold;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_pushRulesTableWrapper {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
tr > th:first-child {
|
||||
text-align: left;
|
||||
font-size: $font-18px;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_pushRulesTable {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
tr > th:nth-child(n + 2) {
|
||||
color: $secondary-fg-color;
|
||||
font-size: $font-12px;
|
||||
vertical-align: middle;
|
||||
width: 66px;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_pushRulesTable thead {
|
||||
font-weight: bold;
|
||||
}
|
||||
tr > td:nth-child(n + 2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_pushRulesTable tbody th {
|
||||
font-weight: 400;
|
||||
}
|
||||
tr > td {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_pushRulesTable tbody th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
// Override StyledRadioButton default styles
|
||||
.mx_RadioButton {
|
||||
justify-content: center;
|
||||
|
||||
.mx_UserNotifSettings_keywords {
|
||||
cursor: pointer;
|
||||
color: $accent-color;
|
||||
}
|
||||
.mx_RadioButton_content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_devicesTable td {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.mx_RadioButton_spacer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserNotifSettings_notifTable {
|
||||
display: table;
|
||||
position: relative;
|
||||
}
|
||||
.mx_UserNotifSettings_floatingSection {
|
||||
margin-top: 40px;
|
||||
|
||||
.mx_UserNotifSettings_notifTable .mx_Spinner {
|
||||
position: absolute;
|
||||
}
|
||||
& > div:first-child { // section header
|
||||
font-size: $font-18px;
|
||||
font-weight: $font-semi-bold;
|
||||
}
|
||||
|
||||
.mx_NotificationSound_soundUpload {
|
||||
display: none;
|
||||
}
|
||||
> table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
margin-top: 8px;
|
||||
|
||||
.mx_NotificationSound_browse {
|
||||
color: $accent-color;
|
||||
border: 1px solid $accent-color;
|
||||
background-color: transparent;
|
||||
}
|
||||
tr > td:first-child {
|
||||
// Just for a bit of spacing
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NotificationSound_save {
|
||||
margin-left: 5px;
|
||||
color: white;
|
||||
background-color: $accent-color;
|
||||
}
|
||||
.mx_UserNotifSettings_clearNotifsButton {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mx_NotificationSound_resetSound {
|
||||
margin-top: 5px;
|
||||
color: white;
|
||||
border: $warning-color;
|
||||
background-color: $warning-color;
|
||||
.mx_TagComposer {
|
||||
margin-top: 35px; // lots of distance from the last line of the table
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 classNames from 'classnames';
|
||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||
import url from 'url';
|
||||
import katex from 'katex';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
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 {
|
||||
try {
|
||||
const parsed = url.parse(inputUrl);
|
||||
if (!parsed.protocol) return false;
|
||||
// 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) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import { createClient } from 'matrix-js-sdk/src/matrix';
|
|||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
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 SecurityCustomisations from "./customisations/Security";
|
||||
|
@ -65,7 +66,7 @@ interface ILoadSessionOpts {
|
|||
guestIsUrl?: string;
|
||||
ignoreGuest?: boolean;
|
||||
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");
|
||||
return doSetLoggedIn({
|
||||
userId: fragmentQueryParams.guest_user_id,
|
||||
accessToken: fragmentQueryParams.guest_access_token,
|
||||
userId: fragmentQueryParams.guest_user_id as string,
|
||||
accessToken: fragmentQueryParams.guest_access_token as string,
|
||||
homeserverUrl: guestHsUrl,
|
||||
identityServerUrl: guestIsUrl,
|
||||
guest: true,
|
||||
|
@ -173,7 +174,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
|||
* login, else false
|
||||
*/
|
||||
export function attemptTokenLogin(
|
||||
queryParams: Record<string, string>,
|
||||
queryParams: QueryDict,
|
||||
defaultDeviceDisplayName?: string,
|
||||
fragmentAfterLogin?: string,
|
||||
): Promise<boolean> {
|
||||
|
@ -198,7 +199,7 @@ export function attemptTokenLogin(
|
|||
homeserver,
|
||||
identityServer,
|
||||
"m.login.token", {
|
||||
token: queryParams.loginToken,
|
||||
token: queryParams.loginToken as string,
|
||||
initial_device_display_name: defaultDeviceDisplayName,
|
||||
},
|
||||
).then(function(creds) {
|
||||
|
|
|
@ -328,7 +328,7 @@ export const Notifier = {
|
|||
|
||||
onEvent: function(ev: MatrixEvent) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -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
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
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
|
||||
// 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"
|
||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
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 });
|
||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||
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)
|
||||
return () => _t("%(senderName)s made no change", { senderName });
|
||||
} else {
|
||||
|
@ -319,7 +318,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
|
|||
});
|
||||
}
|
||||
|
||||
function textForCallAnswerEvent(event): () => string | null {
|
||||
function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
|
||||
return () => {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
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 eventContent = event.getContent();
|
||||
let getReason = () => "";
|
||||
|
@ -364,14 +363,14 @@ function textForCallHangupEvent(event): () => string | null {
|
|||
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
||||
}
|
||||
|
||||
function textForCallRejectEvent(event): () => string | null {
|
||||
function textForCallRejectEvent(event: MatrixEvent): () => string | null {
|
||||
return () => {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
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');
|
||||
// FIXME: Find a better way to determine this from the event?
|
||||
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();
|
||||
|
||||
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();
|
||||
switch (event.getContent().history_visibility) {
|
||||
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
|
||||
function textForPowerEvent(event): () => string | null {
|
||||
function textForPowerEvent(event: MatrixEvent): () => string | null {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
if (!event.getPrevContent() || !event.getPrevContent().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 });
|
||||
}
|
||||
|
||||
function textForWidgetEvent(event): () => string | null {
|
||||
function textForWidgetEvent(event: MatrixEvent): () => string | null {
|
||||
const senderName = event.getSender();
|
||||
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
|
||||
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();
|
||||
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 { entity: prevEntity } = event.getPrevContent();
|
||||
const { entity, recommendation, reason } = event.getContent();
|
||||
|
@ -646,7 +645,9 @@ function textForMjolnirEvent(event): () => string | null {
|
|||
}
|
||||
|
||||
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 = {
|
||||
|
@ -682,14 +683,27 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
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()];
|
||||
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, allowJSX: true): string | JSX.Element;
|
||||
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element {
|
||||
export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
|
||||
export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
|
||||
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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -63,9 +63,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
|||
// https://github.com/vector-im/element-web/issues/2427
|
||||
// ...and possibly some of the others at
|
||||
// https://github.com/vector-im/element-web/issues/3363
|
||||
if (room.timeline.length &&
|
||||
room.timeline[room.timeline.length - 1].sender &&
|
||||
room.timeline[room.timeline.length - 1].sender.userId === myUserId) {
|
||||
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { createClient } from "matrix-js-sdk/src/matrix";
|
|||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
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
|
||||
import 'focus-visible';
|
||||
|
@ -105,6 +105,8 @@ import VerificationRequestToast from '../views/toasts/VerificationRequestToast';
|
|||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||
import SoftLogout from './auth/SoftLogout';
|
||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import { copyPlaintext } from "../../utils/strings";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -153,7 +155,7 @@ const ONBOARDING_FLOW_STARTERS = [
|
|||
|
||||
interface IScreen {
|
||||
screen: string;
|
||||
params?: object;
|
||||
params?: QueryDict;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -183,9 +185,9 @@ interface IProps { // TODO type things better
|
|||
onNewScreen: (screen: string, replaceLast: boolean) => void;
|
||||
enableGuest?: boolean;
|
||||
// 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
|
||||
startingFragmentQueryParams?: Record<string, string>;
|
||||
startingFragmentQueryParams?: QueryDict;
|
||||
// called when we have completed a token login
|
||||
onTokenLoginCompleted?: () => void;
|
||||
// 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.
|
||||
defaultDeviceDisplayName?: string;
|
||||
// A function that makes a registration URL
|
||||
makeRegistrationUrl: (object) => string;
|
||||
makeRegistrationUrl: (params: QueryDict) => string;
|
||||
}
|
||||
|
||||
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']) {
|
||||
// probably a threepid invite - try to store it
|
||||
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':
|
||||
this.forgetRoom(payload.room_id);
|
||||
break;
|
||||
case 'copy_room':
|
||||
this.copyRoom(payload.room_id);
|
||||
break;
|
||||
case 'reject_invite':
|
||||
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
|
||||
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
|
||||
* @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 });
|
||||
};
|
||||
|
||||
private makeRegistrationUrl = (params: {[key: string]: string}) => {
|
||||
private makeRegistrationUrl = (params: QueryDict) => {
|
||||
if (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}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
fragmentAfterLogin={fragmentAfterLogin}
|
||||
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername}
|
||||
defaultUsername={this.props.startingFragmentQueryParams.defaultUsername as string}
|
||||
{...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
|
||||
// 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
|
||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
@ -239,7 +243,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
// 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.showTypingNotificationsWatcherRef =
|
||||
|
@ -399,17 +404,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
return !this.isMounted;
|
||||
};
|
||||
|
||||
private get showHiddenEvents(): boolean {
|
||||
return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
|
||||
}
|
||||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
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)
|
||||
}
|
||||
|
||||
if (this.showHiddenEventsInTimeline) {
|
||||
if (this.showHiddenEvents) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!haveTileForEvent(mxEv)) {
|
||||
if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
|
||||
return false; // no tile = no show
|
||||
}
|
||||
|
||||
|
@ -569,7 +578,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
|
|||
|
||||
if (grouper) {
|
||||
if (grouper.shouldGroup(mxEv)) {
|
||||
grouper.add(mxEv);
|
||||
grouper.add(mxEv, this.showHiddenEvents);
|
||||
continue;
|
||||
} else {
|
||||
// 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?
|
||||
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
|
||||
const continuation = !wantsDateSeparator &&
|
||||
shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
@ -946,7 +956,7 @@ abstract class BaseGrouper {
|
|||
}
|
||||
|
||||
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 getNewPrevEvent(): MatrixEvent;
|
||||
}
|
||||
|
@ -1200,10 +1210,10 @@ class MemberGrouper extends BaseGrouper {
|
|||
return membershipTypes.includes(ev.getType() as EventType);
|
||||
}
|
||||
|
||||
public add(ev: MatrixEvent): void {
|
||||
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
|
||||
if (ev.getType() === EventType.RoomMember) {
|
||||
// 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(
|
||||
ev.getId(),
|
||||
|
|
|
@ -166,6 +166,7 @@ export interface IState {
|
|||
canReply: boolean;
|
||||
layout: Layout;
|
||||
lowBandwidth: boolean;
|
||||
showHiddenEventsInTimeline: boolean;
|
||||
showReadReceipts: boolean;
|
||||
showRedactions: boolean;
|
||||
showJoinLeaves: boolean;
|
||||
|
@ -230,6 +231,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canReply: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||
showReadReceipts: true,
|
||||
showRedactions: 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("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this.context.on("Event.decrypted", this.onEventDecrypted);
|
||||
this.context.on("event", this.onEvent);
|
||||
// Start listening for RoomViewStore updates
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
||||
|
@ -268,6 +269,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
SettingsStore.watchSetting("lowBandwidth", null, () =>
|
||||
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("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this.context.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
this.context.removeListener("event", this.onEvent);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -837,8 +840,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (this.unmounted) return;
|
||||
|
||||
// ignore events for other rooms
|
||||
if (!room) return;
|
||||
if (!this.state.room || room.roomId != this.state.room.roomId) return;
|
||||
if (!room || room.roomId !== this.state.room?.roomId) return;
|
||||
|
||||
// ignore events from filtered timelines
|
||||
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.
|
||||
if (this.state.joining) return;
|
||||
|
||||
if (!ev.isBeingDecrypted() && !ev.isDecryptionFailure()) {
|
||||
this.handleEffects(ev);
|
||||
}
|
||||
|
||||
if (ev.getSender() !== this.context.credentials.userId) {
|
||||
// update unread count when scrolled up
|
||||
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;
|
||||
this.handleEffects(ev);
|
||||
};
|
||||
|
||||
private onEvent = (ev) => {
|
||||
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
|
||||
|
||||
private handleEffects = (ev: MatrixEvent) => {
|
||||
const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room);
|
||||
if (!notifState.isUnread) return;
|
||||
|
||||
|
@ -1393,7 +1393,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!haveTileForEvent(mxEv)) {
|
||||
if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
|
||||
// XXX: can this ever happen? It will make the result count
|
||||
// not match the displayed count.
|
||||
continue;
|
||||
|
|
|
@ -555,9 +555,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
// more than the timeout on userActiveRecently.
|
||||
//
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const sender = ev.sender ? ev.sender.userId : null;
|
||||
callRMUpdated = false;
|
||||
if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
||||
if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
||||
updatedState.readMarkerVisible = true;
|
||||
} else if (lastLiveEvent && this.getReadMarkerPosition() === 0) {
|
||||
// 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;
|
||||
for (i++; i < events.length; i++) {
|
||||
const ev = events[i];
|
||||
if (!ev.sender || ev.sender.userId != myUserId) {
|
||||
if (ev.getSender() !== myUserId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1337,8 +1336,9 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const shouldIgnore = !!ev.status || // local echo
|
||||
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
|
||||
const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
|
||||
(ignoreOwn && ev.getSender() === myUserId); // own message
|
||||
const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
|
||||
shouldHideEvent(ev, this.context);
|
||||
|
||||
if (isWithoutTile || !node) {
|
||||
// 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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -26,11 +27,11 @@ interface IProps {
|
|||
|
||||
@replaceableComponent("views.messages.TextualEvent")
|
||||
export default class TextualEvent extends React.Component<IProps> {
|
||||
render() {
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true);
|
||||
if (!text || (text as string).length === 0) return null;
|
||||
return (
|
||||
<div className="mx_TextualEvent">{ text }</div>
|
||||
);
|
||||
static contextType = RoomContext;
|
||||
|
||||
public render() {
|
||||
const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
|
||||
if (!text) return null;
|
||||
return <div className="mx_TextualEvent">{ text }</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1160,7 +1160,7 @@ function isMessageEvent(ev) {
|
|||
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
|
||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
|
@ -1170,7 +1170,7 @@ export function haveTileForEvent(e) {
|
|||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
if (handler === 'messages.TextualEvent') {
|
||||
return hasText(e);
|
||||
return hasText(e, showHiddenEvents);
|
||||
} else if (handler === 'messages.RoomCreate') {
|
||||
return Boolean(e.getContent()['predecessor']);
|
||||
} else {
|
||||
|
|
|
@ -408,10 +408,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
this.setState({ addRoomContextMenuPosition: null });
|
||||
};
|
||||
|
||||
private onUnreadFirstChanged = async () => {
|
||||
private onUnreadFirstChanged = () => {
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === 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
|
||||
};
|
||||
|
||||
|
|
|
@ -358,6 +358,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
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) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -408,7 +419,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("Use default")}
|
||||
label={_t("Global")}
|
||||
active={state === ALL_MESSAGES}
|
||||
iconClassName="mx_RoomTile_iconBell"
|
||||
onClick={this.onClickAllNotifs}
|
||||
|
@ -517,6 +528,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
iconClassName="mx_RoomTile_iconInvite"
|
||||
/>
|
||||
) : null}
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onCopyRoomClick}
|
||||
label={_t("Copy Link")}
|
||||
iconClassName="mx_RoomTile_iconCopyLink"
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onOpenRoomSettings}
|
||||
label={_t("Settings")}
|
||||
|
|
|
@ -15,14 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||
import EventTile, { haveTileForEvent } from "./EventTile";
|
||||
import DateSeparator from '../messages/DateSeparator';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import DateSeparator from "../messages/DateSeparator";
|
||||
import EventTile, { haveTileForEvent } from "./EventTile";
|
||||
|
||||
interface IProps {
|
||||
// a matrix-js-sdk SearchResult containing the details of this result
|
||||
|
@ -37,6 +38,8 @@ interface IProps {
|
|||
|
||||
@replaceableComponent("views.rooms.SearchResultTile")
|
||||
export default class SearchResultTile extends React.Component<IProps> {
|
||||
static contextType = RoomContext;
|
||||
|
||||
public render() {
|
||||
const result = this.props.searchResult;
|
||||
const mxEv = result.context.getEvent();
|
||||
|
@ -44,7 +47,10 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
|
||||
const ts1 = mxEv.getTs();
|
||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||
const layout = SettingsStore.getValue("layout");
|
||||
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
|
||||
|
||||
const timeline = result.context.getTimeline();
|
||||
for (let j = 0; j < timeline.length; j++) {
|
||||
|
@ -54,26 +60,25 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
if (!contextual) {
|
||||
highlights = this.props.searchHighlights;
|
||||
}
|
||||
if (haveTileForEvent(ev)) {
|
||||
ret.push((
|
||||
if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
|
||||
ret.push(
|
||||
<EventTile
|
||||
key={`${eventId}+${j}`}
|
||||
mxEvent={ev}
|
||||
layout={layout}
|
||||
contextual={contextual}
|
||||
highlights={highlights}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
highlightLink={this.props.resultLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
isTwelveHour={isTwelveHour}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
));
|
||||
enableFlair={enableFlair}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li data-scroll-tokens={eventId}>
|
||||
{ ret }
|
||||
</li>);
|
||||
|
||||
return <li data-scroll-tokens={eventId}>{ 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
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 { _t } from "../../../../../languageHandler";
|
||||
import * as sdk from "../../../../../index";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import Notifications from "../../Notifications";
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
|
||||
export default class NotificationUserSettingsTab extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
render() {
|
||||
const Notifications = sdk.getComponent("views.settings.Notifications");
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_NotificationUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
|
|
@ -41,6 +41,7 @@ const RoomContext = createContext<IState>({
|
|||
canReply: false,
|
||||
layout: Layout.Group,
|
||||
lowBandwidth: false,
|
||||
showHiddenEventsInTimeline: false,
|
||||
showReadReceipts: true,
|
||||
showRedactions: true,
|
||||
showJoinLeaves: true,
|
||||
|
|
|
@ -1133,33 +1133,24 @@
|
|||
"Connecting to integration manager...": "Connecting 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.",
|
||||
"Error saving email notification preferences": "Error saving email notification preferences",
|
||||
"An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.",
|
||||
"Keywords": "Keywords",
|
||||
"Enter keywords separated by a comma:": "Enter keywords separated by a comma:",
|
||||
"Failed to change settings": "Failed to change settings",
|
||||
"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.",
|
||||
"Messages containing keywords": "Messages containing keywords",
|
||||
"Error saving notification preferences": "Error saving notification preferences",
|
||||
"An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.",
|
||||
"Enable for this account": "Enable for this account",
|
||||
"Enable email notifications for %(email)s": "Enable email notifications for %(email)s",
|
||||
"Enable desktop notifications for this session": "Enable desktop notifications for this session",
|
||||
"Show message in desktop notification": "Show message in desktop notification",
|
||||
"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",
|
||||
"Off": "Off",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -1658,7 +1649,6 @@
|
|||
"Show %(count)s more|other": "Show %(count)s more",
|
||||
"Show %(count)s more|one": "Show %(count)s more",
|
||||
"Show less": "Show less",
|
||||
"Use default": "Use default",
|
||||
"All messages": "All messages",
|
||||
"Mentions & Keywords": "Mentions & Keywords",
|
||||
"Notification options": "Notification options",
|
||||
|
@ -1667,6 +1657,7 @@
|
|||
"Favourite": "Favourite",
|
||||
"Low Priority": "Low Priority",
|
||||
"Invite People": "Invite People",
|
||||
"Copy Link": "Copy Link",
|
||||
"Leave Room": "Leave Room",
|
||||
"Room options": "Room options",
|
||||
"%(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 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",
|
||||
"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",
|
||||
"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",
|
||||
|
|
|
@ -67,7 +67,6 @@ export default class EventIndex extends EventEmitter {
|
|||
|
||||
client.on('sync', this.onSync);
|
||||
client.on('Room.timeline', this.onRoomTimeline);
|
||||
client.on('Event.decrypted', this.onEventDecrypted);
|
||||
client.on('Room.timelineReset', this.onTimelineReset);
|
||||
client.on('Room.redaction', this.onRedaction);
|
||||
client.on('RoomState.events', this.onRoomStateEvent);
|
||||
|
@ -82,7 +81,6 @@ export default class EventIndex extends EventEmitter {
|
|||
|
||||
client.removeListener('sync', this.onSync);
|
||||
client.removeListener('Room.timeline', this.onRoomTimeline);
|
||||
client.removeListener('Event.decrypted', this.onEventDecrypted);
|
||||
client.removeListener('Room.timelineReset', this.onTimelineReset);
|
||||
client.removeListener('Room.redaction', this.onRedaction);
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -15,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { PushRuleVectorState, State } from "./PushRuleVectorState";
|
||||
import { IExtendedPushRule, IRuleSets } from "./types";
|
||||
import { PushRuleVectorState, VectorState } from "./PushRuleVectorState";
|
||||
import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||
|
||||
export interface IContentRules {
|
||||
vectorState: State;
|
||||
rules: IExtendedPushRule[];
|
||||
externalRules: IExtendedPushRule[];
|
||||
vectorState: VectorState;
|
||||
rules: IAnnotatedPushRule[];
|
||||
externalRules: IAnnotatedPushRule[];
|
||||
}
|
||||
|
||||
export const SCOPE = "global";
|
||||
|
@ -39,9 +38,9 @@ export class ContentRules {
|
|||
* externalRules: a list of other keyword rules, with states other than
|
||||
* vectorState
|
||||
*/
|
||||
static parseContentRules(rulesets: IRuleSets): IContentRules {
|
||||
public static parseContentRules(rulesets: IPushRules): IContentRules {
|
||||
// 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.
|
||||
// Vector displays a single global rule for a list of keywords
|
||||
|
@ -59,7 +58,7 @@ export class ContentRules {
|
|||
|
||||
if (contentRules.loud.length) {
|
||||
return {
|
||||
vectorState: State.Loud,
|
||||
vectorState: VectorState.Loud,
|
||||
rules: contentRules.loud,
|
||||
externalRules: [
|
||||
...contentRules.loud_but_disabled,
|
||||
|
@ -70,33 +69,33 @@ export class ContentRules {
|
|||
};
|
||||
} else if (contentRules.loud_but_disabled.length) {
|
||||
return {
|
||||
vectorState: State.Off,
|
||||
vectorState: VectorState.Off,
|
||||
rules: contentRules.loud_but_disabled,
|
||||
externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other],
|
||||
};
|
||||
} else if (contentRules.on.length) {
|
||||
return {
|
||||
vectorState: State.On,
|
||||
vectorState: VectorState.On,
|
||||
rules: contentRules.on,
|
||||
externalRules: [...contentRules.on_but_disabled, ...contentRules.other],
|
||||
};
|
||||
} else if (contentRules.on_but_disabled.length) {
|
||||
return {
|
||||
vectorState: State.Off,
|
||||
vectorState: VectorState.Off,
|
||||
rules: contentRules.on_but_disabled,
|
||||
externalRules: contentRules.other,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
vectorState: State.On,
|
||||
vectorState: VectorState.On,
|
||||
rules: [],
|
||||
externalRules: contentRules.other,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static _categoriseContentRules(rulesets: IRuleSets) {
|
||||
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = {
|
||||
private static categoriseContentRules(rulesets: IPushRules) {
|
||||
const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IAnnotatedPushRule[]> = {
|
||||
on: [],
|
||||
on_but_disabled: [],
|
||||
loud: [],
|
||||
|
@ -109,7 +108,7 @@ export class ContentRules {
|
|||
const r = rulesets.global[kind][i];
|
||||
|
||||
// check it's not a default rule
|
||||
if (r.rule_id[0] === '.' || kind !== "content") {
|
||||
if (r.rule_id[0] === '.' || kind !== PushRuleKind.ContentSpecific) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -117,14 +116,14 @@ export class ContentRules {
|
|||
r.kind = kind;
|
||||
|
||||
switch (PushRuleVectorState.contentRuleVectorStateKind(r)) {
|
||||
case State.On:
|
||||
case VectorState.On:
|
||||
if (r.enabled) {
|
||||
contentRules.on.push(r);
|
||||
} else {
|
||||
contentRules.on_but_disabled.push(r);
|
||||
}
|
||||
break;
|
||||
case State.Loud:
|
||||
case VectorState.Loud:
|
||||
if (r.enabled) {
|
||||
contentRules.loud.push(r);
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -15,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Action, Actions } from "./types";
|
||||
import { PushRuleAction, PushRuleActionName, TweakHighlight, TweakSound } from "matrix-js-sdk/src/@types/PushRules";
|
||||
|
||||
interface IEncodedActions {
|
||||
notify: boolean;
|
||||
|
@ -30,23 +29,23 @@ export class NotificationUtils {
|
|||
// "highlight: true/false,
|
||||
// }
|
||||
// to a list of push actions.
|
||||
static encodeActions(action: IEncodedActions) {
|
||||
static encodeActions(action: IEncodedActions): PushRuleAction[] {
|
||||
const notify = action.notify;
|
||||
const sound = action.sound;
|
||||
const highlight = action.highlight;
|
||||
if (notify) {
|
||||
const actions: Action[] = [Actions.Notify];
|
||||
const actions: PushRuleAction[] = [PushRuleActionName.Notify];
|
||||
if (sound) {
|
||||
actions.push({ "set_tweak": "sound", "value": sound });
|
||||
actions.push({ "set_tweak": "sound", "value": sound } as TweakSound);
|
||||
}
|
||||
if (highlight) {
|
||||
actions.push({ "set_tweak": "highlight" });
|
||||
actions.push({ "set_tweak": "highlight" } as TweakHighlight);
|
||||
} else {
|
||||
actions.push({ "set_tweak": "highlight", "value": false });
|
||||
actions.push({ "set_tweak": "highlight", "value": false } as TweakHighlight);
|
||||
}
|
||||
return actions;
|
||||
} else {
|
||||
return [Actions.DontNotify];
|
||||
return [PushRuleActionName.DontNotify];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,16 +55,16 @@ export class NotificationUtils {
|
|||
// "highlight: true/false,
|
||||
// }
|
||||
// If the actions couldn't be decoded then returns null.
|
||||
static decodeActions(actions: Action[]): IEncodedActions {
|
||||
static decodeActions(actions: PushRuleAction[]): IEncodedActions {
|
||||
let notify = false;
|
||||
let sound = null;
|
||||
let highlight = false;
|
||||
|
||||
for (let i = 0; i < actions.length; ++i) {
|
||||
const action = actions[i];
|
||||
if (action === Actions.Notify) {
|
||||
if (action === PushRuleActionName.Notify) {
|
||||
notify = true;
|
||||
} else if (action === Actions.DontNotify) {
|
||||
} else if (action === PushRuleActionName.DontNotify) {
|
||||
notify = false;
|
||||
} else if (typeof action === "object") {
|
||||
if (action.set_tweak === "sound") {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -17,9 +16,9 @@ limitations under the License.
|
|||
|
||||
import { StandardActions } from "./StandardActions";
|
||||
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 */
|
||||
Off = "off",
|
||||
/** The user will receive push notification for this rule */
|
||||
|
@ -31,26 +30,26 @@ export enum State {
|
|||
|
||||
export class PushRuleVectorState {
|
||||
// Backwards compatibility (things should probably be using the enum above instead)
|
||||
static OFF = State.Off;
|
||||
static ON = State.On;
|
||||
static LOUD = State.Loud;
|
||||
static OFF = VectorState.Off;
|
||||
static ON = VectorState.On;
|
||||
static LOUD = VectorState.Loud;
|
||||
|
||||
/**
|
||||
* Enum for state of a push rule as defined by the Vector UI.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
static states = State;
|
||||
static states = VectorState;
|
||||
|
||||
/**
|
||||
* Convert a PushRuleVectorState to a list of actions
|
||||
*
|
||||
* @return [object] list of push-rule actions
|
||||
*/
|
||||
static actionsFor(pushRuleVectorState: State) {
|
||||
if (pushRuleVectorState === State.On) {
|
||||
static actionsFor(pushRuleVectorState: VectorState) {
|
||||
if (pushRuleVectorState === VectorState.On) {
|
||||
return StandardActions.ACTION_NOTIFY;
|
||||
} else if (pushRuleVectorState === State.Loud) {
|
||||
} else if (pushRuleVectorState === VectorState.Loud) {
|
||||
return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND;
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +61,7 @@ export class PushRuleVectorState {
|
|||
* category or in PushRuleVectorState.LOUD, regardless of its enabled
|
||||
* 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);
|
||||
|
||||
if (!decoded) {
|
||||
|
@ -80,10 +79,10 @@ export class PushRuleVectorState {
|
|||
let stateKind = null;
|
||||
switch (tweaks) {
|
||||
case 0:
|
||||
stateKind = State.On;
|
||||
stateKind = VectorState.On;
|
||||
break;
|
||||
case 2:
|
||||
stateKind = State.Loud;
|
||||
stateKind = VectorState.Loud;
|
||||
break;
|
||||
}
|
||||
return stateKind;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
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.
|
||||
|
@ -17,19 +16,24 @@ limitations under the License.
|
|||
|
||||
import { _td } from '../languageHandler';
|
||||
import { StandardActions } from "./StandardActions";
|
||||
import { PushRuleVectorState } from "./PushRuleVectorState";
|
||||
import { PushRuleVectorState, VectorState } from "./PushRuleVectorState";
|
||||
import { NotificationUtils } from "./NotificationUtils";
|
||||
import { PushRuleAction, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
|
||||
|
||||
type StateToActionsMap = {
|
||||
[state in VectorState]?: PushRuleAction[];
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
kind: Kind;
|
||||
kind: PushRuleKind;
|
||||
description: string;
|
||||
vectorStateToActions: Action;
|
||||
vectorStateToActions: StateToActionsMap;
|
||||
}
|
||||
|
||||
class VectorPushRuleDefinition {
|
||||
private kind: Kind;
|
||||
private kind: PushRuleKind;
|
||||
private description: string;
|
||||
private vectorStateToActions: Action;
|
||||
public readonly vectorStateToActions: StateToActionsMap;
|
||||
|
||||
constructor(opts: IProps) {
|
||||
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.
|
||||
*/
|
||||
export const VectorPushRulesDefinitions = {
|
||||
// Messages containing user's display name
|
||||
".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
|
||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Messages containing user's username (localpart/MXID)
|
||||
".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
|
||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Messages containing @room
|
||||
".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
|
||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_HIGHLIGHT,
|
||||
off: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT,
|
||||
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Messages just sent to the user in a 1:1 room
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||
},
|
||||
}),
|
||||
|
||||
// Encrypted messages just sent to the user in a 1:1 room
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
[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
|
||||
// By opposition, all other room messages are from group chat rooms.
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
[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
|
||||
// By opposition, all other room messages are from group chat rooms.
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||
},
|
||||
}),
|
||||
|
||||
// Invitation for the user
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Incoming call
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_NOTIFY_RING_SOUND,
|
||||
off: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_RING_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DISABLED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Notifications from bots
|
||||
".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
|
||||
vectorStateToActions: {
|
||||
// .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI
|
||||
on: StandardActions.ACTION_DISABLED,
|
||||
loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
off: StandardActions.ACTION_DONT_NOTIFY,
|
||||
[VectorState.On]: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND,
|
||||
[VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY,
|
||||
},
|
||||
}),
|
||||
|
||||
// Room upgrades (tombstones)
|
||||
".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
|
||||
vectorStateToActions: { // The actions for each vector state, or null to disable the rule.
|
||||
on: StandardActions.ACTION_NOTIFY,
|
||||
loud: StandardActions.ACTION_HIGHLIGHT,
|
||||
off: StandardActions.ACTION_DISABLED,
|
||||
[VectorState.On]: StandardActions.ACTION_NOTIFY,
|
||||
[VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT,
|
||||
[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.
|
||||
console.log("Regenerating room lists: Startup");
|
||||
await this.readAndCacheSettingsFromStore();
|
||||
await this.regenerateAllLists({ trigger: false });
|
||||
await this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed
|
||||
this.regenerateAllLists({ trigger: false });
|
||||
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.trigger();
|
||||
|
@ -150,7 +150,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
await this.updateState({
|
||||
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
|
||||
* 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
|
||||
|
||||
const activeRoomId = RoomViewStore.getRoomId();
|
||||
if (!activeRoomId && this.algorithm.stickyRoom) {
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
this.algorithm.setStickyRoom(null);
|
||||
} else if (activeRoomId) {
|
||||
const activeRoom = this.matrixClient.getRoom(activeRoomId);
|
||||
if (!activeRoom) {
|
||||
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) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
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");
|
||||
await this.readAndCacheSettingsFromStore();
|
||||
|
||||
await this.regenerateAllLists({ trigger: false }); // regenerate the lists now
|
||||
this.regenerateAllLists({ trigger: false }); // regenerate the lists now
|
||||
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
|
||||
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
|
||||
|
@ -377,7 +377,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
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
|
||||
}
|
||||
|
||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
||||
const shouldUpdate = this.algorithm.handleRoomUpdate(room, cause);
|
||||
if (shouldUpdate) {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// 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
|
||||
// doesn't freak out.
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
await this.algorithm.setKnownRooms(rooms);
|
||||
this.algorithm.setStickyRoom(null);
|
||||
this.algorithm.setKnownRooms(rooms);
|
||||
|
||||
// Set the sticky room back, if needed, now that we have updated the store.
|
||||
// This will use relative stickyness to the new room set.
|
||||
if (stickyIsStillPresent) {
|
||||
await this.algorithm.setStickyRoom(currentSticky);
|
||||
this.algorithm.setStickyRoom(currentSticky);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
await this.setAndPersistTagSorting(tagId, sort);
|
||||
this.setAndPersistTagSorting(tagId, sort);
|
||||
this.updateFn.trigger();
|
||||
}
|
||||
|
||||
private async setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
await this.algorithm.setTagSorting(tagId, sort);
|
||||
private setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
this.algorithm.setTagSorting(tagId, sort);
|
||||
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
||||
localStorage.setItem(`mx_tagSort_${tagId}`, sort);
|
||||
}
|
||||
|
@ -520,13 +520,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return tagSort;
|
||||
}
|
||||
|
||||
public async setListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
await this.setAndPersistListOrder(tagId, order);
|
||||
public setListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
this.setAndPersistListOrder(tagId, order);
|
||||
this.updateFn.trigger();
|
||||
}
|
||||
|
||||
private async setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
await this.algorithm.setListOrdering(tagId, order);
|
||||
private setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||
this.algorithm.setListOrdering(tagId, order);
|
||||
// TODO: Per-account? https://github.com/vector-im/element-web/issues/14114
|
||||
localStorage.setItem(`mx_listOrder_${tagId}`, order);
|
||||
}
|
||||
|
@ -563,7 +563,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
return listOrder;
|
||||
}
|
||||
|
||||
private async updateAlgorithmInstances() {
|
||||
private updateAlgorithmInstances() {
|
||||
// We'll require an update, so mark for one. Marking now also prevents the calls
|
||||
// to setTagSorting and setListOrder from causing triggers.
|
||||
this.updateFn.mark();
|
||||
|
@ -576,10 +576,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const listOrder = this.calculateListOrder(tag);
|
||||
|
||||
if (tagSort !== definedSort) {
|
||||
await this.setAndPersistTagSorting(tag, tagSort);
|
||||
this.setAndPersistTagSorting(tag, tagSort);
|
||||
}
|
||||
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
|
||||
* 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");
|
||||
|
||||
const rooms = this.getPlausibleRooms();
|
||||
|
@ -656,8 +656,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
|
||||
}
|
||||
|
||||
await this.algorithm.populateTags(sorts, orders);
|
||||
await this.algorithm.setKnownRooms(rooms);
|
||||
this.algorithm.populateTags(sorts, orders);
|
||||
this.algorithm.setKnownRooms(rooms);
|
||||
|
||||
this.initialListsGenerated = true;
|
||||
|
||||
|
|
|
@ -16,8 +16,9 @@ limitations under the License.
|
|||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||
import {
|
||||
|
@ -122,8 +123,8 @@ export class Algorithm extends EventEmitter {
|
|||
* Awaitable version of the sticky room setter.
|
||||
* @param val The new room to sticky.
|
||||
*/
|
||||
public async setStickyRoom(val: Room) {
|
||||
await this.updateStickyRoom(val);
|
||||
public setStickyRoom(val: Room) {
|
||||
this.updateStickyRoom(val);
|
||||
}
|
||||
|
||||
public getTagSorting(tagId: TagID): SortAlgorithm {
|
||||
|
@ -131,13 +132,13 @@ export class Algorithm extends EventEmitter {
|
|||
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 (!sort) throw new Error("Algorithm must be defined");
|
||||
this.sortAlgorithms[tagId] = sort;
|
||||
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tagId];
|
||||
await algorithm.setSortAlgorithm(sort);
|
||||
algorithm.setSortAlgorithm(sort);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||
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];
|
||||
}
|
||||
|
||||
public async setListOrdering(tagId: TagID, order: ListAlgorithm) {
|
||||
public setListOrdering(tagId: TagID, order: ListAlgorithm) {
|
||||
if (!tagId) throw new Error("Tag ID must be defined");
|
||||
if (!order) throw new Error("Algorithm must be defined");
|
||||
this.listAlgorithms[tagId] = order;
|
||||
|
@ -156,7 +157,7 @@ export class Algorithm extends EventEmitter {
|
|||
const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]);
|
||||
this.algorithms[tagId] = algorithm;
|
||||
|
||||
await algorithm.setRooms(this._cachedRooms[tagId]);
|
||||
algorithm.setRooms(this._cachedRooms[tagId]);
|
||||
this._cachedRooms[tagId] = algorithm.orderedRooms;
|
||||
this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list
|
||||
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() {
|
||||
await this.recalculateFilteredRooms();
|
||||
private handleFilterChange() {
|
||||
this.recalculateFilteredRooms();
|
||||
|
||||
// re-emit the update so the list store can fire an off-cycle update if needed
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(FILTER_CHANGED);
|
||||
}
|
||||
|
||||
private async updateStickyRoom(val: Room) {
|
||||
try {
|
||||
return await this.doUpdateStickyRoom(val);
|
||||
} finally {
|
||||
this._lastStickyRoom = null; // clear to indicate we're done changing
|
||||
}
|
||||
private updateStickyRoom(val: Room) {
|
||||
this.doUpdateStickyRoom(val);
|
||||
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") {
|
||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||
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)) {
|
||||
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
|
||||
|
||||
// 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;
|
||||
|
@ -269,10 +264,10 @@ export class Algorithm extends EventEmitter {
|
|||
// referential checks as the references can differ through the lifecycle.
|
||||
if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) {
|
||||
// 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
|
||||
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
|
||||
// it is still the same room.
|
||||
|
@ -462,9 +457,8 @@ export class Algorithm extends EventEmitter {
|
|||
* them.
|
||||
* @param {ITagSortingMap} tagSortingMap The tags to generate.
|
||||
* @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 (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`);
|
||||
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
|
||||
* previously known information and instead use these rooms instead.
|
||||
* @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 (!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
|
||||
// avoid accidentally duplicating it in the list.
|
||||
const oldStickyRoom = this._stickyRoom;
|
||||
await this.updateStickyRoom(null);
|
||||
if (oldStickyRoom) this.updateStickyRoom(null);
|
||||
|
||||
this.rooms = rooms;
|
||||
|
||||
|
@ -541,7 +534,7 @@ export class Algorithm extends EventEmitter {
|
|||
|
||||
// If we can avoid doing work, do so.
|
||||
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;
|
||||
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.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
|
||||
// we also have to update the position of it.
|
||||
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.tag !== oldStickyRoom.tag) {
|
||||
// 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
|
||||
* will already have the rooms which belong to it - they just need ordering. Must
|
||||
* 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");
|
||||
|
||||
for (const tag of Object.keys(updatedTagMap)) {
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[tag];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
await algorithm.setRooms(updatedTagMap[tag]);
|
||||
algorithm.setRooms(updatedTagMap[tag]);
|
||||
updatedTagMap[tag] = algorithm.orderedRooms;
|
||||
}
|
||||
}
|
||||
|
@ -673,11 +665,10 @@ export class Algorithm extends EventEmitter {
|
|||
* may no-op this request if no changes are required.
|
||||
* @param {Room} room The room which might have affected sorting.
|
||||
* @param {RoomUpdateCause} cause The reason for the update being triggered.
|
||||
* @returns {Promise<boolean>} A promise which resolve to true or false
|
||||
* depending on whether or not getOrderedRooms() should be called after
|
||||
* processing.
|
||||
* @returns {Promise<boolean>} A boolean of whether or not getOrderedRooms()
|
||||
* should be called after processing.
|
||||
*/
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
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");
|
||||
|
||||
// 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) {
|
||||
const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room;
|
||||
const isForLastSticky = this._lastStickyRoom?.room === room;
|
||||
const roomTags = this.roomIdsToTags[room.roomId];
|
||||
const hasTags = roomTags && roomTags.length > 0;
|
||||
|
||||
|
@ -744,7 +735,7 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
const algorithm: OrderingAlgorithm = this.algorithms[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.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list
|
||||
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];
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -789,7 +780,7 @@ export class Algorithm extends EventEmitter {
|
|||
};
|
||||
} else {
|
||||
// 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];
|
||||
if (!algorithm) throw new Error(`No algorithm for ${tag}`);
|
||||
|
||||
await algorithm.handleRoomUpdate(room, cause);
|
||||
algorithm.handleRoomUpdate(room, cause);
|
||||
this._cachedRooms[tag] = algorithm.orderedRooms;
|
||||
|
||||
// Flag that we've done something
|
||||
|
|
|
@ -94,15 +94,15 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
return state.color;
|
||||
}
|
||||
|
||||
public async setRooms(rooms: Room[]): Promise<any> {
|
||||
public setRooms(rooms: Room[]): void {
|
||||
if (this.sortingAlgorithm === SortAlgorithm.Manual) {
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
} else {
|
||||
// Every other sorting type affects the categories, not the whole tag.
|
||||
const categorized = this.categorizeRooms(rooms);
|
||||
for (const category of Object.keys(categorized)) {
|
||||
const roomsToOrder = categorized[category];
|
||||
categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
|
||||
categorized[category] = sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm);
|
||||
}
|
||||
|
||||
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) {
|
||||
const category = this.getRoomCategory(room);
|
||||
this.alterCategoryPositionBy(category, 1, this.indices);
|
||||
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) {
|
||||
const roomIdx = this.getRoomIndex(room);
|
||||
if (roomIdx === -1) {
|
||||
|
@ -141,55 +141,49 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
return true;
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
try {
|
||||
await this.updateLock.acquireAsync();
|
||||
|
||||
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();
|
||||
public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
|
||||
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
|
||||
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
|
||||
// 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
|
||||
|
@ -201,7 +195,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
|||
const startIdx = this.indices[category];
|
||||
const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,42 +29,32 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
|
|||
super(tagId, initialSortingAlgorithm);
|
||||
}
|
||||
|
||||
public async setRooms(rooms: Room[]): Promise<any> {
|
||||
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
public setRooms(rooms: Room[]): void {
|
||||
this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm);
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room, cause): Promise<boolean> {
|
||||
try {
|
||||
await this.updateLock.acquireAsync();
|
||||
|
||||
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
|
||||
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();
|
||||
public handleRoomUpdate(room, cause): boolean {
|
||||
const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
|
||||
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 = 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 { RoomUpdateCause, TagID } from "../../models";
|
||||
import { SortAlgorithm } from "../models";
|
||||
import AwaitLock from "await-lock";
|
||||
|
||||
/**
|
||||
* Represents a list ordering algorithm. Subclasses should populate the
|
||||
|
@ -26,7 +25,6 @@ import AwaitLock from "await-lock";
|
|||
export abstract class OrderingAlgorithm {
|
||||
protected cachedOrderedRooms: Room[];
|
||||
protected sortingAlgorithm: SortAlgorithm;
|
||||
protected readonly updateLock = new AwaitLock();
|
||||
|
||||
protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
|
@ -45,21 +43,20 @@ export abstract class OrderingAlgorithm {
|
|||
* @param newAlgorithm The new algorithm. Must be defined.
|
||||
* @returns Resolves when complete.
|
||||
*/
|
||||
public async setSortAlgorithm(newAlgorithm: SortAlgorithm) {
|
||||
public setSortAlgorithm(newAlgorithm: SortAlgorithm) {
|
||||
if (!newAlgorithm) throw new Error("A sorting algorithm must be defined");
|
||||
this.sortingAlgorithm = newAlgorithm;
|
||||
|
||||
// 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
|
||||
* of the ordering.
|
||||
* @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
|
||||
|
@ -69,7 +66,7 @@ export abstract class OrderingAlgorithm {
|
|||
* @param cause The cause of the update.
|
||||
* @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 {
|
||||
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.
|
||||
*/
|
||||
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 compare(a.name, b.name);
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ export interface IAlgorithm {
|
|||
* Sorts the given rooms according to the sorting rules of the algorithm.
|
||||
* @param {Room[]} rooms The rooms to sort.
|
||||
* @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.
|
||||
*/
|
||||
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;
|
||||
return rooms.sort((a, b) => {
|
||||
return getOrderProp(a) - getOrderProp(b);
|
||||
|
|
|
@ -97,7 +97,7 @@ export const sortRooms = (rooms: Room[]): Room[] => {
|
|||
* useful to the user.
|
||||
*/
|
||||
export class RecentAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
|
||||
return sortRooms(rooms);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@ export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorith
|
|||
* @param {Room[]} rooms The rooms to sort.
|
||||
* @param {TagID} tagId The tag in which the sorting is occurring.
|
||||
* @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);
|
||||
}
|
||||
|
|
|
@ -14,9 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import url from 'url';
|
||||
import qs from 'qs';
|
||||
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
|
||||
|
@ -28,11 +25,8 @@ export function getHostingLink(campaign) {
|
|||
if (MatrixClientPeg.get().getDomain() !== 'matrix.org') return null;
|
||||
|
||||
try {
|
||||
const hostingUrl = url.parse(hostingLink);
|
||||
const params = qs.parse(hostingUrl.query);
|
||||
params.utm_campaign = campaign;
|
||||
hostingUrl.search = undefined;
|
||||
hostingUrl.query = params;
|
||||
const hostingUrl = new URL(hostingLink);
|
||||
hostingUrl.searchParams.set("utm_campaign", campaign);
|
||||
return hostingUrl.format();
|
||||
} catch (e) {
|
||||
return hostingLink;
|
||||
|
|
|
@ -96,6 +96,7 @@ export function createTestClient() {
|
|||
},
|
||||
},
|
||||
decryptEventIfNeeded: () => Promise.resolve(),
|
||||
isUserIgnored: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue