Implement dialog for resending local echo transactions
This commit is contained in:
parent
14b0def143
commit
c5574219bb
11 changed files with 289 additions and 9 deletions
|
@ -76,6 +76,7 @@
|
|||
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeDialog.scss";
|
||||
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
||||
@import "./views/dialogs/_ServerOfflineDialog.scss";
|
||||
@import "./views/dialogs/_SetEmailDialog.scss";
|
||||
@import "./views/dialogs/_SetMxIdDialog.scss";
|
||||
@import "./views/dialogs/_SetPasswordDialog.scss";
|
||||
|
@ -216,6 +217,7 @@
|
|||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
@import "./views/verification/_VerificationShowSas.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
|
|
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 |
122
src/components/views/dialogs/ServerOfflineDialog.tsx
Normal file
122
src/components/views/dialogs/ServerOfflineDialog.tsx
Normal 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>;
|
||||
}
|
||||
}
|
|
@ -16,13 +16,23 @@ 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 {
|
||||
render() {
|
||||
private openDialog = () => {
|
||||
Modal.createTrackedDialog('Local Echo Server Error', '', ServerOfflineDialog, {});
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="mx_NonUrgentEchoFailureToast">
|
||||
{_t("Your server isn't responding to some <a>requests</a>", {}, {
|
||||
'a': (sub) => <a>{sub}</a>
|
||||
<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>
|
||||
)
|
||||
|
|
|
@ -612,7 +612,7 @@
|
|||
"Headphones": "Headphones",
|
||||
"Folder": "Folder",
|
||||
"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)",
|
||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||
"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.",
|
||||
"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 />.",
|
||||
"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?",
|
||||
"Clear Storage and Sign Out": "Clear Storage and Sign Out",
|
||||
"Send Logs": "Send Logs",
|
||||
|
@ -1852,7 +1865,6 @@
|
|||
"Reject invitation": "Reject 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",
|
||||
"Resend": "Resend",
|
||||
"Resend edit": "Resend edit",
|
||||
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
|
||||
"Resend removal": "Resend removal",
|
||||
|
|
|
@ -59,13 +59,18 @@ export abstract class CachedEcho<C extends EchoContext, K, V> extends EventEmitt
|
|||
|
||||
private decacheKey(key: K) {
|
||||
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.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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
||||
txn.when(TransactionStatus.Pending, () => this.cacheVal(key, targetVal, txn))
|
||||
.when(TransactionStatus.DoneError, () => this.decacheKey(key))
|
||||
.when(TransactionStatus.DoneError, () => revertFn());
|
||||
|
||||
txn.run();
|
||||
|
|
|
@ -28,7 +28,6 @@ export enum ContextTransactionState {
|
|||
export abstract class EchoContext extends Whenable<ContextTransactionState> implements IDestroyable {
|
||||
private _transactions: EchoTransaction[] = [];
|
||||
private _state = ContextTransactionState.NotStarted;
|
||||
public readonly startTime: Date = new Date();
|
||||
|
||||
public get transactions(): EchoTransaction[] {
|
||||
return arrayFastClone(this._transactions);
|
||||
|
@ -38,6 +37,19 @@ export abstract class EchoContext extends Whenable<ContextTransactionState> impl
|
|||
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);
|
||||
|
|
|
@ -22,7 +22,7 @@ import { RoomEchoContext } from "./RoomEchoContext";
|
|||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { ContextTransactionState } from "./EchoContext";
|
||||
import { ContextTransactionState, EchoContext } from "./EchoContext";
|
||||
import NonUrgentToastStore, { ToastReference } from "../NonUrgentToastStore";
|
||||
import NonUrgentEchoFailureToast from "../../components/views/toasts/NonUrgentEchoFailureToast";
|
||||
|
||||
|
@ -50,6 +50,10 @@ export class EchoStore extends AsyncStoreWithClient<IState> {
|
|||
return EchoStore._instance;
|
||||
}
|
||||
|
||||
public get contexts(): EchoContext[] {
|
||||
return Array.from(this.caches.values()).map(e => e.context);
|
||||
}
|
||||
|
||||
public getOrCreateEchoForRoom(room: Room): RoomCachedEcho {
|
||||
if (this.caches.has(roomContextKey(room))) {
|
||||
return this.caches.get(roomContextKey(room)) as RoomCachedEcho;
|
||||
|
|
|
@ -28,6 +28,8 @@ 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,
|
||||
|
|
Loading…
Reference in a new issue