Implement dialog for resending local echo transactions

This commit is contained in:
Travis Ralston 2020-07-29 20:36:04 -06:00
parent 14b0def143
commit c5574219bb
11 changed files with 289 additions and 9 deletions

View file

@ -76,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";
@ -216,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";

View 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;
}
}
}
}
}
}

View 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;
}
}

View 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

View file

@ -0,0 +1,122 @@
/*
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";
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>];
}
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 for some reason. " +
"Below are some possible reasons why this happened.",
)}</p>
<ul>
<li>{_t("The server took too long to respond.")}</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>;
}
}

View file

@ -16,13 +16,23 @@ limitations under the License.
import React from "react"; import React from "react";
import { _t } from "../../../languageHandler"; 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 { export default class NonUrgentEchoFailureToast extends React.PureComponent {
render() { private openDialog = () => {
Modal.createTrackedDialog('Local Echo Server Error', '', ServerOfflineDialog, {});
};
public render() {
return ( return (
<div className="mx_NonUrgentEchoFailureToast"> <div className="mx_NonUrgentEchoFailureToast">
{_t("Your server isn't responding to some <a>requests</a>", {}, { <span className="mx_NonUrgentEchoFailureToast_icon" />
'a': (sub) => <a>{sub}</a> {_t("Your server isn't responding to some <a>requests</a>.", {}, {
'a': (sub) => (
<AccessibleButton kind="link" onClick={this.openDialog}>{sub}</AccessibleButton>
),
})} })}
</div> </div>
) )

View file

@ -612,7 +612,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>", "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 +1745,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 for some reason. Below are some possible reasons why this happened.": "Your server isn't responding to some of your requests for some reason. Below are some possible reasons why this happened.",
"The server took too long to respond.": "The server 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 +1865,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",

View file

@ -59,13 +59,18 @@ export abstract class CachedEcho<C extends EchoContext, K, V> extends EventEmitt
private decacheKey(key: K) { private decacheKey(key: K) {
if (this.cache.has(key)) { if (this.cache.has(key)) {
this.cache.get(key).txn.cancel(); // should be safe to call this.context.disownTransaction(this.cache.get(key).txn);
this.cache.delete(key); this.cache.delete(key);
this.emit(PROPERTY_UPDATED, key); this.emit(PROPERTY_UPDATED, key);
} }
} }
protected markEchoReceived(key: K) { 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); this.decacheKey(key);
} }
@ -79,7 +84,6 @@ export abstract class CachedEcho<C extends EchoContext, K, V> extends EventEmitt
this.cacheVal(key, targetVal, txn); // set the cache now as it won't be updated by the .when() ladder below. 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)) txn.when(TransactionStatus.Pending, () => this.cacheVal(key, targetVal, txn))
.when(TransactionStatus.DoneError, () => this.decacheKey(key))
.when(TransactionStatus.DoneError, () => revertFn()); .when(TransactionStatus.DoneError, () => revertFn());
txn.run(); txn.run();

View file

@ -28,7 +28,6 @@ export enum ContextTransactionState {
export abstract class EchoContext extends Whenable<ContextTransactionState> implements IDestroyable { export abstract class EchoContext extends Whenable<ContextTransactionState> implements IDestroyable {
private _transactions: EchoTransaction[] = []; private _transactions: EchoTransaction[] = [];
private _state = ContextTransactionState.NotStarted; private _state = ContextTransactionState.NotStarted;
public readonly startTime: Date = new Date();
public get transactions(): EchoTransaction[] { public get transactions(): EchoTransaction[] {
return arrayFastClone(this._transactions); return arrayFastClone(this._transactions);
@ -38,6 +37,19 @@ export abstract class EchoContext extends Whenable<ContextTransactionState> impl
return this._state; 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 { public beginTransaction(auditName: string, runFn: RunFn): EchoTransaction {
const txn = new EchoTransaction(auditName, runFn); const txn = new EchoTransaction(auditName, runFn);
this._transactions.push(txn); this._transactions.push(txn);

View file

@ -22,7 +22,7 @@ import { RoomEchoContext } from "./RoomEchoContext";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
import { ContextTransactionState } from "./EchoContext"; import { ContextTransactionState, EchoContext } from "./EchoContext";
import NonUrgentToastStore, { ToastReference } from "../NonUrgentToastStore"; import NonUrgentToastStore, { ToastReference } from "../NonUrgentToastStore";
import NonUrgentEchoFailureToast from "../../components/views/toasts/NonUrgentEchoFailureToast"; import NonUrgentEchoFailureToast from "../../components/views/toasts/NonUrgentEchoFailureToast";
@ -50,6 +50,10 @@ export class EchoStore extends AsyncStoreWithClient<IState> {
return EchoStore._instance; return EchoStore._instance;
} }
public get contexts(): EchoContext[] {
return Array.from(this.caches.values()).map(e => e.context);
}
public getOrCreateEchoForRoom(room: Room): RoomCachedEcho { public getOrCreateEchoForRoom(room: Room): RoomCachedEcho {
if (this.caches.has(roomContextKey(room))) { if (this.caches.has(roomContextKey(room))) {
return this.caches.get(roomContextKey(room)) as RoomCachedEcho; return this.caches.get(roomContextKey(room)) as RoomCachedEcho;

View file

@ -28,6 +28,8 @@ export class EchoTransaction extends Whenable<TransactionStatus> {
private _status = TransactionStatus.Pending; private _status = TransactionStatus.Pending;
private didFail = false; private didFail = false;
public readonly startTime = new Date();
public constructor( public constructor(
public readonly auditName, public readonly auditName,
public runFn: RunFn, public runFn: RunFn,