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
|
@ -54,6 +54,7 @@ import LeftPanel from "./LeftPanel";
|
|||
import CallContainer from '../views/voip/CallContainer';
|
||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||
|
||||
// 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>
|
||||
</div>
|
||||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
</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.
|
||||
*/
|
||||
|
||||
import React, {createRef} from "react";
|
||||
import React, { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -30,31 +31,26 @@ import {
|
|||
ChevronFace,
|
||||
ContextMenu,
|
||||
ContextMenuTooltipButton,
|
||||
MenuItemRadio,
|
||||
MenuItemCheckbox,
|
||||
MenuItem,
|
||||
MenuItemCheckbox,
|
||||
MenuItemRadio,
|
||||
} from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {
|
||||
getRoomNotifsState,
|
||||
setRoomNotifsState,
|
||||
ALL_MESSAGES,
|
||||
ALL_MESSAGES_LOUD,
|
||||
MENTIONS_ONLY,
|
||||
MUTE,
|
||||
} from "../../../RoomNotifs";
|
||||
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { Volume } from "../../../RoomNotifsTypes";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
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 { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
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 {
|
||||
room: Room;
|
||||
|
@ -112,6 +108,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
private dispatcherRef: string;
|
||||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -130,12 +127,19 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
}
|
||||
|
||||
private onNotificationUpdate = () => {
|
||||
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 {
|
||||
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||
}
|
||||
|
@ -307,17 +311,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// get key before we go async and React discards the nativeEvent
|
||||
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);
|
||||
}
|
||||
this.roomProps.notificationVolume = newState;
|
||||
|
||||
const key = (ev as React.KeyboardEvent).key;
|
||||
if (key === Key.ENTER) {
|
||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||
this.setState({notificationsMenuPosition: null}); // hide the menu
|
||||
|
@ -335,7 +331,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const state = getRoomNotifsState(this.props.room.roomId);
|
||||
const state = this.roomProps.notificationVolume;
|
||||
|
||||
let contextMenu = null;
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue