Merge pull request #5065 from matrix-org/travis/echo/audit
Add local echo for notifications in the new room list
This commit is contained in:
commit
af49639bd8
24 changed files with 1105 additions and 30 deletions
39
docs/local-echo-dev.md
Normal file
39
docs/local-echo-dev.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Local echo (developer docs)
|
||||||
|
|
||||||
|
The React SDK provides some local echo functionality to allow for components to do something
|
||||||
|
quickly and fall back when it fails. This is all available in the `local-echo` directory within
|
||||||
|
`stores`.
|
||||||
|
|
||||||
|
Echo is handled in EchoChambers, with `GenericEchoChamber` being the base implementation for all
|
||||||
|
chambers. The `EchoChamber` class is provided as semantic access to a `GenericEchoChamber`
|
||||||
|
implementation, such as the `RoomEchoChamber` (which handles echoable details of a room).
|
||||||
|
|
||||||
|
Anything that can be locally echoed will be provided by the `GenericEchoChamber` implementation.
|
||||||
|
The echo chamber will also need to deal with external changes, and has full control over whether
|
||||||
|
or not something has successfully been echoed.
|
||||||
|
|
||||||
|
An `EchoContext` is provided to echo chambers (usually with a matching type: `RoomEchoContext`
|
||||||
|
gets provided to a `RoomEchoChamber` for example) with details about their intended area of
|
||||||
|
effect, as well as manage `EchoTransaction`s. An `EchoTransaction` is simply a unit of work that
|
||||||
|
needs to be locally echoed.
|
||||||
|
|
||||||
|
The `EchoStore` manages echo chamber instances, builds contexts, and is generally less semantically
|
||||||
|
accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things
|
||||||
|
tidy, this is an intentional design decision.
|
||||||
|
|
||||||
|
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
|
||||||
|
`EventEmitter`. Whenables are ways of actioning a changing condition without having to deal
|
||||||
|
with listeners being torn down. Once the reference count of the Whenable causes garbage collection,
|
||||||
|
the Whenable's listeners will also be torn down. This is accelerated by the `IDestroyable` interface
|
||||||
|
usage.
|
||||||
|
|
||||||
|
## Audit functionality
|
||||||
|
|
||||||
|
The UI supports a "Server isn't responding" dialog which includes a partial audit log-like
|
||||||
|
structure to it. This is partially the reason for added complexity of `EchoTransaction`s
|
||||||
|
and `EchoContext`s - this information feeds the UI states which then provide direct retry
|
||||||
|
mechanisms.
|
||||||
|
|
||||||
|
The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left)
|
||||||
|
is set up, where the dialog then drives through the contexts and transactions.
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
@import "./structures/_MainSplit.scss";
|
@import "./structures/_MainSplit.scss";
|
||||||
@import "./structures/_MatrixChat.scss";
|
@import "./structures/_MatrixChat.scss";
|
||||||
@import "./structures/_MyGroups.scss";
|
@import "./structures/_MyGroups.scss";
|
||||||
|
@import "./structures/_NonUrgentToastContainer.scss";
|
||||||
@import "./structures/_NotificationPanel.scss";
|
@import "./structures/_NotificationPanel.scss";
|
||||||
@import "./structures/_RightPanel.scss";
|
@import "./structures/_RightPanel.scss";
|
||||||
@import "./structures/_RoomDirectory.scss";
|
@import "./structures/_RoomDirectory.scss";
|
||||||
|
@ -75,6 +76,7 @@
|
||||||
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
||||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||||
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
||||||
|
@import "./views/dialogs/_ServerOfflineDialog.scss";
|
||||||
@import "./views/dialogs/_SetEmailDialog.scss";
|
@import "./views/dialogs/_SetEmailDialog.scss";
|
||||||
@import "./views/dialogs/_SetMxIdDialog.scss";
|
@import "./views/dialogs/_SetMxIdDialog.scss";
|
||||||
@import "./views/dialogs/_SetPasswordDialog.scss";
|
@import "./views/dialogs/_SetPasswordDialog.scss";
|
||||||
|
@ -215,6 +217,7 @@
|
||||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
|
|
35
res/css/structures/_NonUrgentToastContainer.scss
Normal file
35
res/css/structures/_NonUrgentToastContainer.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_NonUrgentToastContainer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 28px;
|
||||||
|
z-index: 101; // same level as other toasts
|
||||||
|
|
||||||
|
.mx_NonUrgentToastContainer_toast {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 320px;
|
||||||
|
font-size: $font-13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
// We don't use variables on the colours because we want it to be the same
|
||||||
|
// in all themes.
|
||||||
|
background-color: #17191c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
72
res/css/views/dialogs/_ServerOfflineDialog.scss
Normal file
72
res/css/views/dialogs/_ServerOfflineDialog.scss
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ServerOfflineDialog {
|
||||||
|
.mx_ServerOfflineDialog_content {
|
||||||
|
padding-right: 85px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: $primary-fg-color;
|
||||||
|
opacity: 0.1;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
li:nth-child(n + 2) {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ServerOfflineDialog_content_context {
|
||||||
|
.mx_ServerOfflineDialog_content_context_timestamp {
|
||||||
|
display: inline-block;
|
||||||
|
width: 115px;
|
||||||
|
color: $muted-fg-color;
|
||||||
|
line-height: 24px; // same as avatar
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ServerOfflineDialog_content_context_timeline {
|
||||||
|
display: inline-block;
|
||||||
|
width: calc(100% - 155px); // 115px timestamp width + 40px right margin
|
||||||
|
|
||||||
|
.mx_ServerOfflineDialog_content_context_timeline_header {
|
||||||
|
span {
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ServerOfflineDialog_content_context_txn {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.mx_ServerOfflineDialog_content_context_txn_desc {
|
||||||
|
width: calc(100% - 100px); // 100px is an arbitrary margin for the button
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
float: right;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
res/css/views/toasts/_NonUrgentEchoFailureToast.scss
Normal file
37
res/css/views/toasts/_NonUrgentEchoFailureToast.scss
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_NonUrgentEchoFailureToast {
|
||||||
|
.mx_NonUrgentEchoFailureToast_icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: $font-18px;
|
||||||
|
height: $font-18px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background-color: #fff; // we know that non-urgent toasts are always styled the same
|
||||||
|
mask-image: url('$(res)/img/element-icons/cloud-off.svg');
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span { // includes the i18n block
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
3
res/img/element-icons/cloud-off.svg
Normal file
3
res/img/element-icons/cloud-off.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.53033 0.46967C1.23744 0.176777 0.762563 0.176777 0.46967 0.46967C0.176777 0.762563 0.176777 1.23744 0.46967 1.53033L4.3982 5.45886C3.81109 6.13809 3.38896 7.01315 3.21555 7.99387C1.96379 8.20624 1 9.465 1 10.981C1 12.6455 2.16209 14 3.59014 14H12.9393L16.4697 17.5303C16.7626 17.8232 17.2374 17.8232 17.5303 17.5303C17.8232 17.2374 17.8232 16.7626 17.5303 16.4697L1.53033 0.46967ZM17 10.9817C16.998 11.8303 16.6946 12.5985 16.2081 13.1475L7.07635 4.01569C7.18805 4.00529 7.30083 4 7.41451 4C8.75982 4 9.99711 4.71787 10.8072 5.94503C11.0993 5.85476 11.4011 5.80939 11.7058 5.80939C13.0303 5.80939 14.2138 6.65743 14.8199 8.00337C16.0519 8.23522 17 9.48685 17 10.9817Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 839 B |
|
@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { JSXElementConstructor } from "react";
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||||
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
||||||
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||||
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
|
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
||||||
|
|
|
@ -54,6 +54,7 @@ import LeftPanel from "./LeftPanel";
|
||||||
import CallContainer from '../views/voip/CallContainer';
|
import CallContainer from '../views/voip/CallContainer';
|
||||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
|
@ -688,6 +689,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
|
<NonUrgentToastContainer />
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
63
src/components/structures/NonUrgentToastContainer.tsx
Normal file
63
src/components/structures/NonUrgentToastContainer.tsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
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 * as React from "react";
|
||||||
|
import { ComponentClass } from "../../@types/common";
|
||||||
|
import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
|
||||||
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
toasts: ComponentClass[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
|
||||||
|
public constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
toasts: NonUrgentToastStore.instance.components,
|
||||||
|
};
|
||||||
|
|
||||||
|
NonUrgentToastStore.instance.on(UPDATE_EVENT, this.onUpdateToasts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
NonUrgentToastStore.instance.off(UPDATE_EVENT, this.onUpdateToasts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onUpdateToasts = () => {
|
||||||
|
this.setState({toasts: NonUrgentToastStore.instance.components});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const toasts = this.state.toasts.map((t, i) => {
|
||||||
|
return (
|
||||||
|
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
|
||||||
|
{React.createElement(t, {})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_NonUrgentToastContainer" role="alert">
|
||||||
|
{toasts}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
124
src/components/views/dialogs/ServerOfflineDialog.tsx
Normal file
124
src/components/views/dialogs/ServerOfflineDialog.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
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 * as React from 'react';
|
||||||
|
import BaseDialog from './BaseDialog';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { EchoStore } from "../../../stores/local-echo/EchoStore";
|
||||||
|
import { formatTime } from "../../../DateUtils";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { RoomEchoContext } from "../../../stores/local-echo/RoomEchoContext";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import { TransactionStatus } from "../../../stores/local-echo/EchoTransaction";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (bool) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ServerOfflineDialog extends React.PureComponent<IProps> {
|
||||||
|
public componentDidMount() {
|
||||||
|
EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
EchoStore.instance.off(UPDATE_EVENT, this.onEchosUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEchosUpdated = () => {
|
||||||
|
this.forceUpdate(); // no state to worry about
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderTimeline(): React.ReactElement[] {
|
||||||
|
return EchoStore.instance.contexts.map((c, i) => {
|
||||||
|
if (!c.firstFailedTime) return null; // not useful
|
||||||
|
if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c);
|
||||||
|
const header = (
|
||||||
|
<div className="mx_ServerOfflineDialog_content_context_timeline_header">
|
||||||
|
<RoomAvatar width={24} height={24} room={c.room} />
|
||||||
|
<span>{c.room.name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const entries = c.transactions
|
||||||
|
.filter(t => t.status === TransactionStatus.DoneError || t.didPreviouslyFail)
|
||||||
|
.map((t, j) => {
|
||||||
|
let button = <Spinner w={19} h={19} />;
|
||||||
|
if (t.status === TransactionStatus.DoneError) {
|
||||||
|
button = (
|
||||||
|
<AccessibleButton kind="link" onClick={() => t.run()}>{_t("Resend")}</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx_ServerOfflineDialog_content_context_txn" key={`txn-${j}`}>
|
||||||
|
<span className="mx_ServerOfflineDialog_content_context_txn_desc">
|
||||||
|
{t.auditName}
|
||||||
|
</span>
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="mx_ServerOfflineDialog_content_context" key={`context-${i}`}>
|
||||||
|
<div className="mx_ServerOfflineDialog_content_context_timestamp">
|
||||||
|
{formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
|
||||||
|
</div>
|
||||||
|
<div className="mx_ServerOfflineDialog_content_context_timeline">
|
||||||
|
{header}
|
||||||
|
{entries}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check
|
||||||
|
if (timeline.length === 0) {
|
||||||
|
timeline = [<div key={1}>{_t("You're all caught up.")}</div>];
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverName = MatrixClientPeg.getHomeserverName();
|
||||||
|
return <BaseDialog title={_t("Server isn't responding")}
|
||||||
|
className='mx_ServerOfflineDialog'
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
hasCancel={true}
|
||||||
|
>
|
||||||
|
<div className="mx_ServerOfflineDialog_content">
|
||||||
|
<p>{_t(
|
||||||
|
"Your server isn't responding to some of your requests. " +
|
||||||
|
"Below are some of the most likely reasons.",
|
||||||
|
)}</p>
|
||||||
|
<ul>
|
||||||
|
<li>{_t("The server (%(serverName)s) took too long to respond.", {serverName})}</li>
|
||||||
|
<li>{_t("Your firewall or anti-virus is blocking the request.")}</li>
|
||||||
|
<li>{_t("A browser extension is preventing the request.")}</li>
|
||||||
|
<li>{_t("The server is offline.")}</li>
|
||||||
|
<li>{_t("The server has denied your request.")}</li>
|
||||||
|
<li>{_t("Your area is experiencing difficulties connecting to the internet.")}</li>
|
||||||
|
<li>{_t("A connection error occurred while trying to contact the server.")}</li>
|
||||||
|
<li>{_t("The server is not configured to indicate what the problem is (CORS).")}</li>
|
||||||
|
</ul>
|
||||||
|
<hr />
|
||||||
|
<h2>{_t("Recent changes that have not yet been received")}</h2>
|
||||||
|
{timeline}
|
||||||
|
</div>
|
||||||
|
</BaseDialog>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,12 +17,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {createRef} from "react";
|
import React, { createRef } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -30,31 +31,26 @@ import {
|
||||||
ChevronFace,
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuTooltipButton,
|
ContextMenuTooltipButton,
|
||||||
MenuItemRadio,
|
|
||||||
MenuItemCheckbox,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
MenuItemCheckbox,
|
||||||
|
MenuItemRadio,
|
||||||
} from "../../structures/ContextMenu";
|
} from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import {
|
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
|
||||||
getRoomNotifsState,
|
|
||||||
setRoomNotifsState,
|
|
||||||
ALL_MESSAGES,
|
|
||||||
ALL_MESSAGES_LOUD,
|
|
||||||
MENTIONS_ONLY,
|
|
||||||
MUTE,
|
|
||||||
} from "../../../RoomNotifs";
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { Volume } from "../../../RoomNotifsTypes";
|
import { Volume } from "../../../RoomNotifsTypes";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||||
import RoomListActions from "../../../actions/RoomListActions";
|
import RoomListActions from "../../../actions/RoomListActions";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import {ActionPayload} from "../../../dispatcher/payloads";
|
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
||||||
|
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
|
||||||
|
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -112,6 +108,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private roomTileRef = createRef<HTMLDivElement>();
|
private roomTileRef = createRef<HTMLDivElement>();
|
||||||
private notificationState: NotificationState;
|
private notificationState: NotificationState;
|
||||||
|
private roomProps: RoomEchoChamber;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -130,12 +127,19 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||||
|
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||||
|
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNotificationUpdate = () => {
|
private onNotificationUpdate = () => {
|
||||||
this.forceUpdate(); // notification state changed - update
|
this.forceUpdate(); // notification state changed - update
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
|
||||||
|
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
|
||||||
|
// else ignore - not important for this tile
|
||||||
|
};
|
||||||
|
|
||||||
private get showContextMenu(): boolean {
|
private get showContextMenu(): boolean {
|
||||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||||
}
|
}
|
||||||
|
@ -307,17 +311,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (MatrixClientPeg.get().isGuest()) return;
|
if (MatrixClientPeg.get().isGuest()) return;
|
||||||
|
|
||||||
// get key before we go async and React discards the nativeEvent
|
this.roomProps.notificationVolume = newState;
|
||||||
const key = (ev as React.KeyboardEvent).key;
|
|
||||||
try {
|
|
||||||
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
|
|
||||||
await setRoomNotifsState(this.props.room.roomId, newState);
|
|
||||||
} catch (error) {
|
|
||||||
// TODO: some form of error notification to the user to inform them that their state change failed.
|
|
||||||
// See https://github.com/vector-im/riot-web/issues/14281
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const key = (ev as React.KeyboardEvent).key;
|
||||||
if (key === Key.ENTER) {
|
if (key === Key.ENTER) {
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
this.setState({notificationsMenuPosition: null}); // hide the menu
|
this.setState({notificationsMenuPosition: null}); // hide the menu
|
||||||
|
@ -335,7 +331,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = getRoomNotifsState(this.props.room.roomId);
|
const state = this.roomProps.notificationVolume;
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (this.state.notificationsMenuPosition) {
|
if (this.state.notificationsMenuPosition) {
|
||||||
|
|
40
src/components/views/toasts/NonUrgentEchoFailureToast.tsx
Normal file
40
src/components/views/toasts/NonUrgentEchoFailureToast.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
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 { _t } from "../../../languageHandler";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import ServerOfflineDialog from "../dialogs/ServerOfflineDialog";
|
||||||
|
|
||||||
|
export default class NonUrgentEchoFailureToast extends React.PureComponent {
|
||||||
|
private openDialog = () => {
|
||||||
|
Modal.createTrackedDialog('Local Echo Server Error', '', ServerOfflineDialog, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className="mx_NonUrgentEchoFailureToast">
|
||||||
|
<span className="mx_NonUrgentEchoFailureToast_icon" />
|
||||||
|
{_t("Your server isn't responding to some <a>requests</a>.", {}, {
|
||||||
|
'a': (sub) => (
|
||||||
|
<AccessibleButton kind="link" onClick={this.openDialog}>{sub}</AccessibleButton>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -443,6 +443,7 @@
|
||||||
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
|
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
|
||||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||||
|
"Change notification settings": "Change notification settings",
|
||||||
"New spinner design": "New spinner design",
|
"New spinner design": "New spinner design",
|
||||||
"Message Pinning": "Message Pinning",
|
"Message Pinning": "Message Pinning",
|
||||||
"Custom user status messages": "Custom user status messages",
|
"Custom user status messages": "Custom user status messages",
|
||||||
|
@ -613,6 +614,7 @@
|
||||||
"Headphones": "Headphones",
|
"Headphones": "Headphones",
|
||||||
"Folder": "Folder",
|
"Folder": "Folder",
|
||||||
"Pin": "Pin",
|
"Pin": "Pin",
|
||||||
|
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
|
||||||
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
|
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
|
||||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||||
|
@ -1745,6 +1747,19 @@
|
||||||
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
|
"Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.",
|
||||||
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.",
|
"This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||||
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
"You'll upgrade this room from <oldVersion /> to <newVersion />.": "You'll upgrade this room from <oldVersion /> to <newVersion />.",
|
||||||
|
"Resend": "Resend",
|
||||||
|
"You're all caught up.": "You're all caught up.",
|
||||||
|
"Server isn't responding": "Server isn't responding",
|
||||||
|
"Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Your server isn't responding to some of your requests. Below are some of the most likely reasons.",
|
||||||
|
"The server (%(serverName)s) took too long to respond.": "The server (%(serverName)s) took too long to respond.",
|
||||||
|
"Your firewall or anti-virus is blocking the request.": "Your firewall or anti-virus is blocking the request.",
|
||||||
|
"A browser extension is preventing the request.": "A browser extension is preventing the request.",
|
||||||
|
"The server is offline.": "The server is offline.",
|
||||||
|
"The server has denied your request.": "The server has denied your request.",
|
||||||
|
"Your area is experiencing difficulties connecting to the internet.": "Your area is experiencing difficulties connecting to the internet.",
|
||||||
|
"A connection error occurred while trying to contact the server.": "A connection error occurred while trying to contact the server.",
|
||||||
|
"The server is not configured to indicate what the problem is (CORS).": "The server is not configured to indicate what the problem is (CORS).",
|
||||||
|
"Recent changes that have not yet been received": "Recent changes that have not yet been received",
|
||||||
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
"Sign out and remove encryption keys?": "Sign out and remove encryption keys?",
|
||||||
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||||
"Send Logs": "Send Logs",
|
"Send Logs": "Send Logs",
|
||||||
|
@ -1852,7 +1867,6 @@
|
||||||
"Reject invitation": "Reject invitation",
|
"Reject invitation": "Reject invitation",
|
||||||
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
|
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
|
||||||
"Unable to reject invite": "Unable to reject invite",
|
"Unable to reject invite": "Unable to reject invite",
|
||||||
"Resend": "Resend",
|
|
||||||
"Resend edit": "Resend edit",
|
"Resend edit": "Resend edit",
|
||||||
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
||||||
"Resend removal": "Resend removal",
|
"Resend removal": "Resend removal",
|
||||||
|
|
|
@ -17,12 +17,25 @@ limitations under the License.
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { AsyncStore } from "./AsyncStore";
|
import { AsyncStore } from "./AsyncStore";
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
|
import { Dispatcher } from "flux";
|
||||||
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
|
||||||
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
|
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
|
||||||
protected matrixClient: MatrixClient;
|
protected matrixClient: MatrixClient;
|
||||||
|
|
||||||
protected abstract async onAction(payload: ActionPayload);
|
protected abstract async onAction(payload: ActionPayload);
|
||||||
|
|
||||||
|
protected constructor(dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
|
||||||
|
super(dispatcher, initialState);
|
||||||
|
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
|
this.matrixClient = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
|
this.onReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected async onReady() {
|
protected async onReady() {
|
||||||
// Default implementation is to do nothing.
|
// Default implementation is to do nothing.
|
||||||
}
|
}
|
||||||
|
@ -42,8 +55,14 @@ export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<
|
||||||
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
|
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.matrixClient !== payload.matrixClient) {
|
||||||
|
if (this.matrixClient) {
|
||||||
|
await this.onNotReady();
|
||||||
|
}
|
||||||
this.matrixClient = payload.matrixClient;
|
this.matrixClient = payload.matrixClient;
|
||||||
await this.onReady();
|
await this.onReady();
|
||||||
|
}
|
||||||
} else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
|
} else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
|
||||||
if (this.matrixClient) {
|
if (this.matrixClient) {
|
||||||
await this.onNotReady();
|
await this.onNotReady();
|
||||||
|
|
50
src/stores/NonUrgentToastStore.ts
Normal file
50
src/stores/NonUrgentToastStore.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
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 EventEmitter from "events";
|
||||||
|
import { ComponentClass } from "../@types/common";
|
||||||
|
import { UPDATE_EVENT } from "./AsyncStore";
|
||||||
|
|
||||||
|
export type ToastReference = symbol;
|
||||||
|
|
||||||
|
export default class NonUrgentToastStore extends EventEmitter {
|
||||||
|
private static _instance: NonUrgentToastStore;
|
||||||
|
|
||||||
|
private toasts = new Map<ToastReference, ComponentClass>();
|
||||||
|
|
||||||
|
public static get instance(): NonUrgentToastStore {
|
||||||
|
if (!NonUrgentToastStore._instance) {
|
||||||
|
NonUrgentToastStore._instance = new NonUrgentToastStore();
|
||||||
|
}
|
||||||
|
return NonUrgentToastStore._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get components(): ComponentClass[] {
|
||||||
|
return Array.from(this.toasts.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addToast(c: ComponentClass): ToastReference {
|
||||||
|
const ref: ToastReference = Symbol();
|
||||||
|
this.toasts.set(ref, c);
|
||||||
|
this.emit(UPDATE_EVENT);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeToast(ref: ToastReference) {
|
||||||
|
this.toasts.delete(ref);
|
||||||
|
this.emit(UPDATE_EVENT);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,9 +15,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import React, {JSXElementConstructor} from "react";
|
import React from "react";
|
||||||
|
import { ComponentClass } from "../@types/common";
|
||||||
|
|
||||||
export interface IToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> {
|
export interface IToast<C extends ComponentClass> {
|
||||||
key: string;
|
key: string;
|
||||||
// higher priority number will be shown on top of lower priority
|
// higher priority number will be shown on top of lower priority
|
||||||
priority: number;
|
priority: number;
|
||||||
|
@ -55,7 +56,7 @@ export default class ToastStore extends EventEmitter {
|
||||||
*
|
*
|
||||||
* @param {object} newToast The new toast
|
* @param {object} newToast The new toast
|
||||||
*/
|
*/
|
||||||
addOrReplaceToast<C extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>>(newToast: IToast<C>) {
|
addOrReplaceToast<C extends ComponentClass>(newToast: IToast<C>) {
|
||||||
const oldIndex = this.toasts.findIndex(t => t.key === newToast.key);
|
const oldIndex = this.toasts.findIndex(t => t.key === newToast.key);
|
||||||
if (oldIndex === -1) {
|
if (oldIndex === -1) {
|
||||||
let newIndex = this.toasts.length;
|
let newIndex = this.toasts.length;
|
||||||
|
|
31
src/stores/local-echo/EchoChamber.ts
Normal file
31
src/stores/local-echo/EchoChamber.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
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 { RoomEchoChamber } from "./RoomEchoChamber";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EchoStore } from "./EchoStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic access to local echo
|
||||||
|
*/
|
||||||
|
export class EchoChamber {
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static forRoom(room: Room): RoomEchoChamber {
|
||||||
|
return EchoStore.instance.getOrCreateChamberForRoom(room);
|
||||||
|
}
|
||||||
|
}
|
87
src/stores/local-echo/EchoContext.ts
Normal file
87
src/stores/local-echo/EchoContext.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
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 { EchoTransaction, RunFn, TransactionStatus } from "./EchoTransaction";
|
||||||
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
import { Whenable } from "../../utils/Whenable";
|
||||||
|
|
||||||
|
export enum ContextTransactionState {
|
||||||
|
NotStarted,
|
||||||
|
PendingErrors,
|
||||||
|
AllSuccessful
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class EchoContext extends Whenable<ContextTransactionState> implements IDestroyable {
|
||||||
|
private _transactions: EchoTransaction[] = [];
|
||||||
|
private _state = ContextTransactionState.NotStarted;
|
||||||
|
|
||||||
|
public get transactions(): EchoTransaction[] {
|
||||||
|
return arrayFastClone(this._transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get state(): ContextTransactionState {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get firstFailedTime(): Date {
|
||||||
|
const failedTxn = this.transactions.find(t => t.didPreviouslyFail || t.status === TransactionStatus.DoneError);
|
||||||
|
if (failedTxn) return failedTxn.startTime;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disownTransaction(txn: EchoTransaction) {
|
||||||
|
const idx = this._transactions.indexOf(txn);
|
||||||
|
if (idx >= 0) this._transactions.splice(idx, 1);
|
||||||
|
txn.destroy();
|
||||||
|
this.checkTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public beginTransaction(auditName: string, runFn: RunFn): EchoTransaction {
|
||||||
|
const txn = new EchoTransaction(auditName, runFn);
|
||||||
|
this._transactions.push(txn);
|
||||||
|
txn.whenAnything(this.checkTransactions);
|
||||||
|
|
||||||
|
// We have no intent to call the transaction again if it succeeds (in fact, it'll
|
||||||
|
// be really angry at us if we do), so call that the end of the road for the events.
|
||||||
|
txn.when(TransactionStatus.DoneSuccess, () => txn.destroy());
|
||||||
|
|
||||||
|
return txn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkTransactions = () => {
|
||||||
|
let status = ContextTransactionState.AllSuccessful;
|
||||||
|
for (const txn of this.transactions) {
|
||||||
|
if (txn.status === TransactionStatus.DoneError || txn.didPreviouslyFail) {
|
||||||
|
status = ContextTransactionState.PendingErrors;
|
||||||
|
break;
|
||||||
|
} else if (txn.status === TransactionStatus.Pending) {
|
||||||
|
status = ContextTransactionState.NotStarted;
|
||||||
|
// no break as we might hit something which broke
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._state = status;
|
||||||
|
this.notifyCondition(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
for (const txn of this.transactions) {
|
||||||
|
txn.destroy();
|
||||||
|
}
|
||||||
|
this._transactions = [];
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
104
src/stores/local-echo/EchoStore.ts
Normal file
104
src/stores/local-echo/EchoStore.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
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 { GenericEchoChamber } from "./GenericEchoChamber";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomEchoChamber } from "./RoomEchoChamber";
|
||||||
|
import { RoomEchoContext } from "./RoomEchoContext";
|
||||||
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import { ContextTransactionState, EchoContext } from "./EchoContext";
|
||||||
|
import NonUrgentToastStore, { ToastReference } from "../NonUrgentToastStore";
|
||||||
|
import NonUrgentEchoFailureToast from "../../components/views/toasts/NonUrgentEchoFailureToast";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
toastRef: ToastReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextKey = string;
|
||||||
|
|
||||||
|
const roomContextKey = (room: Room): ContextKey => `room-${room.roomId}`;
|
||||||
|
|
||||||
|
export class EchoStore extends AsyncStoreWithClient<IState> {
|
||||||
|
private static _instance: EchoStore;
|
||||||
|
|
||||||
|
private caches = new Map<ContextKey, GenericEchoChamber<any, any, any>>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(defaultDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): EchoStore {
|
||||||
|
if (!EchoStore._instance) {
|
||||||
|
EchoStore._instance = new EchoStore();
|
||||||
|
}
|
||||||
|
return EchoStore._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get contexts(): EchoContext[] {
|
||||||
|
return Array.from(this.caches.values()).map(e => e.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOrCreateChamberForRoom(room: Room): RoomEchoChamber {
|
||||||
|
if (this.caches.has(roomContextKey(room))) {
|
||||||
|
return this.caches.get(roomContextKey(room)) as RoomEchoChamber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = new RoomEchoContext(room);
|
||||||
|
context.whenAnything(() => this.checkContexts());
|
||||||
|
|
||||||
|
const echo = new RoomEchoChamber(context);
|
||||||
|
echo.setClient(this.matrixClient);
|
||||||
|
this.caches.set(roomContextKey(room), echo);
|
||||||
|
|
||||||
|
return echo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkContexts() {
|
||||||
|
let hasOrHadError = false;
|
||||||
|
for (const echo of this.caches.values()) {
|
||||||
|
hasOrHadError = echo.context.state === ContextTransactionState.PendingErrors;
|
||||||
|
if (hasOrHadError) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOrHadError && !this.state.toastRef) {
|
||||||
|
const ref = NonUrgentToastStore.instance.addToast(NonUrgentEchoFailureToast);
|
||||||
|
await this.updateState({toastRef: ref});
|
||||||
|
} else if (!hasOrHadError && this.state.toastRef) {
|
||||||
|
NonUrgentToastStore.instance.removeToast(this.state.toastRef);
|
||||||
|
await this.updateState({toastRef: null});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onReady(): Promise<any> {
|
||||||
|
if (!this.caches) return; // can only happen during initialization
|
||||||
|
for (const echo of this.caches.values()) {
|
||||||
|
echo.setClient(this.matrixClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onNotReady(): Promise<any> {
|
||||||
|
for (const echo of this.caches.values()) {
|
||||||
|
echo.setClient(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||||
|
// We have nothing to actually listen for
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
72
src/stores/local-echo/EchoTransaction.ts
Normal file
72
src/stores/local-echo/EchoTransaction.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
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 { Whenable } from "../../utils/Whenable";
|
||||||
|
|
||||||
|
export type RunFn = () => Promise<void>;
|
||||||
|
|
||||||
|
export enum TransactionStatus {
|
||||||
|
Pending,
|
||||||
|
DoneSuccess,
|
||||||
|
DoneError,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EchoTransaction extends Whenable<TransactionStatus> {
|
||||||
|
private _status = TransactionStatus.Pending;
|
||||||
|
private didFail = false;
|
||||||
|
|
||||||
|
public readonly startTime = new Date();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly auditName,
|
||||||
|
public runFn: RunFn,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get didPreviouslyFail(): boolean {
|
||||||
|
return this.didFail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get status(): TransactionStatus {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public run() {
|
||||||
|
if (this.status === TransactionStatus.DoneSuccess) {
|
||||||
|
throw new Error("Cannot re-run a successful echo transaction");
|
||||||
|
}
|
||||||
|
this.setStatus(TransactionStatus.Pending);
|
||||||
|
this.runFn()
|
||||||
|
.then(() => this.setStatus(TransactionStatus.DoneSuccess))
|
||||||
|
.catch(() => this.setStatus(TransactionStatus.DoneError));
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancel() {
|
||||||
|
// Success basically means "done"
|
||||||
|
this.setStatus(TransactionStatus.DoneSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setStatus(status: TransactionStatus) {
|
||||||
|
this._status = status;
|
||||||
|
if (status === TransactionStatus.DoneError) {
|
||||||
|
this.didFail = true;
|
||||||
|
} else if (status === TransactionStatus.DoneSuccess) {
|
||||||
|
this.didFail = false;
|
||||||
|
}
|
||||||
|
this.notifyCondition(status);
|
||||||
|
}
|
||||||
|
}
|
91
src/stores/local-echo/GenericEchoChamber.ts
Normal file
91
src/stores/local-echo/GenericEchoChamber.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
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 { EchoContext } from "./EchoContext";
|
||||||
|
import { EchoTransaction, RunFn, TransactionStatus } from "./EchoTransaction";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
|
export async function implicitlyReverted() {
|
||||||
|
// do nothing :D
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROPERTY_UPDATED = "property_updated";
|
||||||
|
|
||||||
|
export abstract class GenericEchoChamber<C extends EchoContext, K, V> extends EventEmitter {
|
||||||
|
private cache = new Map<K, {txn: EchoTransaction, val: V}>();
|
||||||
|
protected matrixClient: MatrixClient;
|
||||||
|
|
||||||
|
protected constructor(public readonly context: C, private lookupFn: (key: K) => V) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setClient(client: MatrixClient) {
|
||||||
|
const oldClient = this.matrixClient;
|
||||||
|
this.matrixClient = client;
|
||||||
|
this.onClientChanged(oldClient, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract onClientChanged(oldClient: MatrixClient, newClient: MatrixClient);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a value. If the key is in flight, the cached value will be returned. If
|
||||||
|
* the key is not in flight then the lookupFn provided to this class will be
|
||||||
|
* called instead.
|
||||||
|
* @param key The key to look up.
|
||||||
|
* @returns The value for the key.
|
||||||
|
*/
|
||||||
|
public getValue(key: K): V {
|
||||||
|
return this.cache.has(key) ? this.cache.get(key).val : this.lookupFn(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cacheVal(key: K, val: V, txn: EchoTransaction) {
|
||||||
|
this.cache.set(key, {txn, val});
|
||||||
|
this.emit(PROPERTY_UPDATED, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private decacheKey(key: K) {
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.context.disownTransaction(this.cache.get(key).txn);
|
||||||
|
this.cache.delete(key);
|
||||||
|
this.emit(PROPERTY_UPDATED, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected markEchoReceived(key: K) {
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
const txn = this.cache.get(key).txn;
|
||||||
|
this.context.disownTransaction(txn);
|
||||||
|
txn.cancel();
|
||||||
|
}
|
||||||
|
this.decacheKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setValue(auditName: string, key: K, targetVal: V, runFn: RunFn, revertFn: RunFn) {
|
||||||
|
// Cancel any pending transactions for the same key
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.cache.get(key).txn.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
const txn = this.context.beginTransaction(auditName, runFn);
|
||||||
|
this.cacheVal(key, targetVal, txn); // set the cache now as it won't be updated by the .when() ladder below.
|
||||||
|
|
||||||
|
txn.when(TransactionStatus.Pending, () => this.cacheVal(key, targetVal, txn))
|
||||||
|
.when(TransactionStatus.DoneError, () => revertFn());
|
||||||
|
|
||||||
|
txn.run();
|
||||||
|
}
|
||||||
|
}
|
78
src/stores/local-echo/RoomEchoChamber.ts
Normal file
78
src/stores/local-echo/RoomEchoChamber.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
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 { GenericEchoChamber, implicitlyReverted, PROPERTY_UPDATED } from "./GenericEchoChamber";
|
||||||
|
import { getRoomNotifsState, setRoomNotifsState } from "../../RoomNotifs";
|
||||||
|
import { RoomEchoContext } from "./RoomEchoContext";
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
|
import { Volume } from "../../RoomNotifsTypes";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
export type CachedRoomValues = Volume;
|
||||||
|
|
||||||
|
export enum CachedRoomKey {
|
||||||
|
NotificationVolume,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoomEchoChamber extends GenericEchoChamber<RoomEchoContext, CachedRoomKey, CachedRoomValues> {
|
||||||
|
private properties = new Map<CachedRoomKey, CachedRoomValues>();
|
||||||
|
|
||||||
|
public constructor(context: RoomEchoContext) {
|
||||||
|
super(context, (k) => this.properties.get(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onClientChanged(oldClient, newClient) {
|
||||||
|
this.properties.clear();
|
||||||
|
if (oldClient) {
|
||||||
|
oldClient.removeListener("accountData", this.onAccountData);
|
||||||
|
}
|
||||||
|
if (newClient) {
|
||||||
|
// Register the listeners first
|
||||||
|
newClient.on("accountData", this.onAccountData);
|
||||||
|
|
||||||
|
// Then populate the properties map
|
||||||
|
this.updateNotificationVolume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAccountData = (event: MatrixEvent) => {
|
||||||
|
if (event.getType() === "m.push_rules") {
|
||||||
|
const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as Volume;
|
||||||
|
const newVolume = getRoomNotifsState(this.context.room.roomId) as Volume;
|
||||||
|
if (currentVolume !== newVolume) {
|
||||||
|
this.updateNotificationVolume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateNotificationVolume() {
|
||||||
|
this.properties.set(CachedRoomKey.NotificationVolume, getRoomNotifsState(this.context.room.roomId));
|
||||||
|
this.markEchoReceived(CachedRoomKey.NotificationVolume);
|
||||||
|
this.emit(PROPERTY_UPDATED, CachedRoomKey.NotificationVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers below here ----
|
||||||
|
|
||||||
|
public get notificationVolume(): Volume {
|
||||||
|
return this.getValue(CachedRoomKey.NotificationVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set notificationVolume(v: Volume) {
|
||||||
|
this.setValue(_t("Change notification settings"), CachedRoomKey.NotificationVolume, v, async () => {
|
||||||
|
return setRoomNotifsState(this.context.room.roomId, v);
|
||||||
|
}, implicitlyReverted);
|
||||||
|
}
|
||||||
|
}
|
24
src/stores/local-echo/RoomEchoContext.ts
Normal file
24
src/stores/local-echo/RoomEchoContext.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
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 { EchoContext } from "./EchoContext";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
export class RoomEchoContext extends EchoContext {
|
||||||
|
constructor(public readonly room: Room) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
86
src/utils/Whenable.ts
Normal file
86
src/utils/Whenable.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
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 { IDestroyable } from "./IDestroyable";
|
||||||
|
import { arrayFastClone } from "./arrays";
|
||||||
|
|
||||||
|
export type WhenFn<T> = (w: Whenable<T>) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenables are a cheap way to have Observable patterns mixed with typical
|
||||||
|
* usage of Promises, without having to tear down listeners or calls. Whenables
|
||||||
|
* are intended to be used when a condition will be met multiple times and
|
||||||
|
* the consumer needs to know *when* that happens.
|
||||||
|
*/
|
||||||
|
export abstract class Whenable<T> implements IDestroyable {
|
||||||
|
private listeners: {condition: T | null, fn: WhenFn<T>}[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a call to `fn` *when* the `condition` is met.
|
||||||
|
* @param condition The condition to match.
|
||||||
|
* @param fn The function to call.
|
||||||
|
* @returns This.
|
||||||
|
*/
|
||||||
|
public when(condition: T, fn: WhenFn<T>): Whenable<T> {
|
||||||
|
this.listeners.push({condition, fn});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a fall to `fn` *when* any of the `conditions` are met.
|
||||||
|
* @param conditions The conditions to match.
|
||||||
|
* @param fn The function to call.
|
||||||
|
* @returns This.
|
||||||
|
*/
|
||||||
|
public whenAnyOf(conditions: T[], fn: WhenFn<T>): Whenable<T> {
|
||||||
|
for (const condition of conditions) {
|
||||||
|
this.when(condition, fn);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a call to `fn` *when* any condition is met.
|
||||||
|
* @param fn The function to call.
|
||||||
|
* @returns This.
|
||||||
|
*/
|
||||||
|
public whenAnything(fn: WhenFn<T>): Whenable<T> {
|
||||||
|
this.listeners.push({condition: null, fn});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all the whenables of a given condition.
|
||||||
|
* @param condition The new condition that has been met.
|
||||||
|
*/
|
||||||
|
protected notifyCondition(condition: T) {
|
||||||
|
const listeners = arrayFastClone(this.listeners); // clone just in case the handler modifies us
|
||||||
|
for (const listener of listeners) {
|
||||||
|
if (listener.condition === null || listener.condition === condition) {
|
||||||
|
try {
|
||||||
|
listener.fn(this);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error calling whenable listener for ${condition}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.listeners = [];
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue