Merge pull request #5897 from matrix-org/travis/error-states

Improve message error states
This commit is contained in:
Travis Ralston 2021-04-26 08:16:26 -06:00 committed by GitHub
commit ddb8947c15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 368 additions and 225 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_RoomStatusBar { .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
margin-left: 65px; margin-left: 65px;
min-height: 50px; min-height: 50px;
} }
@ -68,6 +68,99 @@ limitations under the License.
min-height: 58px; min-height: 58px;
} }
.mx_RoomStatusBar_unsentMessages {
> div[role="alert"] {
// cheat some basic alignment
display: flex;
align-items: center;
min-height: 70px;
margin: 12px;
padding-left: 16px;
background-color: $header-panel-bg-color;
border-radius: 4px;
}
.mx_RoomStatusBar_unsentBadge {
margin-right: 12px;
.mx_NotificationBadge {
// Override sizing from the default badge
width: 24px !important;
height: 24px !important;
border-radius: 24px !important;
.mx_NotificationBadge_count {
font-size: $font-16px !important; // override default
}
}
}
.mx_RoomStatusBar_unsentTitle {
color: $warning-color;
font-size: $font-15px;
}
.mx_RoomStatusBar_unsentDescription {
font-size: $font-12px;
}
.mx_RoomStatusBar_unsentButtonBar {
flex-grow: 1;
text-align: right;
margin-right: 22px;
color: $muted-fg-color;
.mx_AccessibleButton {
padding: 5px 10px;
padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding
display: inline-block;
position: relative;
&:nth-child(2) {
border-left: 1px solid $resend-button-divider-color;
}
&::before {
content: '';
position: absolute;
left: 10px; // inset for regular button padding
background-color: $muted-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
&.mx_RoomStatusBar_unsentCancelAllBtn::before {
mask-image: url('$(res)/img/element-icons/trashcan.svg');
width: 12px;
height: 16px;
top: calc(50% - 8px); // text sizes are dynamic
}
&.mx_RoomStatusBar_unsentResendAllBtn {
padding-left: 34px; // 28px from above, but +6px to account for the wider icon
&::before {
mask-image: url('$(res)/img/element-icons/retry.svg');
width: 18px;
height: 18px;
top: calc(50% - 9px); // text sizes are dynamic
}
}
}
.mx_InlineSpinner {
vertical-align: middle;
margin-right: 5px;
top: 1px; // just to help the vertical alignment be slightly better
& + span {
margin-right: 10px; // same margin/padding as the rightmost button
}
}
}
}
.mx_RoomStatusBar_connectionLostBar img { .mx_RoomStatusBar_connectionLostBar img {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
@ -103,7 +196,7 @@ limitations under the License.
} }
.mx_MatrixChat_useCompactLayout { .mx_MatrixChat_useCompactLayout {
.mx_RoomStatusBar { .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
min-height: 40px; min-height: 40px;
} }

View file

@ -105,3 +105,11 @@ limitations under the License.
.mx_MessageActionBar_optionsButton::after { .mx_MessageActionBar_optionsButton::after {
mask-image: url('$(res)/img/element-icons/context-menu.svg'); mask-image: url('$(res)/img/element-icons/context-menu.svg');
} }
.mx_MessageActionBar_resendButton::after {
mask-image: url('$(res)/img/element-icons/retry.svg');
}
.mx_MessageActionBar_cancelButton::after {
mask-image: url('$(res)/img/element-icons/trashcan.svg');
}

View file

@ -214,10 +214,6 @@ $left-gutter: 64px;
color: $accent-fg-color; color: $accent-fg-color;
} }
.mx_EventTile_notSent {
color: $event-notsent-color;
}
.mx_EventTile_receiptSent, .mx_EventTile_receiptSent,
.mx_EventTile_receiptSending { .mx_EventTile_receiptSending {
// We don't use `position: relative` on the element because then it won't line // We don't use `position: relative` on the element because then it won't line

View file

@ -0,0 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58365 3.90848C5.79757 2.94852 7.33285 2.375 9 2.375C12.6817 2.375 15.7112 5.1675 16.086 8.75H17.6314C17.9253 8.75 18.1006 9.07792 17.9376 9.32274L15.6812 12.711C15.5355 12.9297 15.2145 12.9297 15.0688 12.711L12.8124 9.32274C12.6494 9.07792 12.8247 8.75 13.1186 8.75H14.5754C14.2088 5.99798 11.8523 3.875 9 3.875C7.68247 3.875 6.4726 4.32705 5.51407 5.08504C5.45221 5.13396 5.39899 5.17326 5.36001 5.20114C5.34047 5.21513 5.32433 5.22637 5.31229 5.23463L5.29733 5.24482L5.29227 5.24821L5.29037 5.24948L5.28958 5.25L5.28923 5.25023L5.28906 5.25034L5.28898 5.2504L4.875 4.625L5.2889 5.25045C4.94347 5.47904 4.47814 5.38433 4.24955 5.0389C4.02136 4.69408 4.11534 4.22977 4.45929 4.00075L4.4633 3.99802C4.46789 3.99487 4.47605 3.9892 4.48719 3.98123C4.5096 3.9652 4.5433 3.94038 4.58365 3.90848ZM3.42456 10.25H4.88138C5.1753 10.25 5.35061 9.92208 5.18758 9.67726L2.93119 6.28905C2.78553 6.07032 2.46447 6.07032 2.31881 6.28905L0.0624241 9.67726C-0.100613 9.92207 0.0746987 10.25 0.368618 10.25H1.914C2.28878 13.8325 5.31828 16.625 9 16.625C10.7415 16.625 12.3388 15.9992 13.5764 14.9611C13.8938 14.6949 13.9353 14.2219 13.6691 13.9045C13.4029 13.5872 12.9298 13.5457 12.6125 13.8119C11.6349 14.6319 10.376 15.125 9 15.125C6.14769 15.125 3.79123 13.002 3.42456 10.25Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,3 @@
<svg width="12" height="17" viewBox="0 0 12 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.857143 14.5C0.857143 15.4491 1.62857 16.5 2.57143 16.5H9.42857C10.3714 16.5 11.1429 15.2542 11.1429 14.3051V5.67692C11.1429 4.72781 10.3714 3.95128 9.42857 3.95128H2.57143C1.62857 3.95128 0.857143 4.72781 0.857143 5.67692V14.5ZM11.1429 1.36282H9L8.39143 0.750218C8.23714 0.59491 8.01429 0.5 7.79143 0.5H4.20857C3.98571 0.5 3.76286 0.59491 3.60857 0.750218L3 1.36282H0.857143C0.385714 1.36282 0 1.75109 0 2.22564C0 2.70019 0.385714 3.08846 0.857143 3.08846H11.1429C11.6143 3.08846 12 2.70019 12 2.22564C12 1.75109 11.6143 1.36282 11.1429 1.36282Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

View file

@ -63,6 +63,8 @@ $input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: $bg-color; $field-focused-label-bg-color: $bg-color;
$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity.
// scrollbars // scrollbars
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
$scrollbar-track-color: transparent; $scrollbar-track-color: transparent;

View file

@ -61,6 +61,8 @@ $input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: $bg-color; $field-focused-label-bg-color: $bg-color;
$resend-button-divider-color: $muted-fg-color;
// scrollbars // scrollbars
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
$scrollbar-track-color: transparent; $scrollbar-track-color: transparent;

View file

@ -97,6 +97,8 @@ $input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: #ffffff; $field-focused-label-bg-color: #ffffff;
$resend-button-divider-color: $input-darker-bg-color;
$button-bg-color: $accent-color; $button-bg-color: $accent-color;
$button-fg-color: white; $button-fg-color: white;

View file

@ -91,6 +91,8 @@ $field-focused-label-bg-color: #ffffff;
$button-bg-color: $accent-color; $button-bg-color: $accent-color;
$button-fg-color: white; $button-fg-color: white;
$resend-button-divider-color: $input-darker-bg-color;
// apart from login forms, which have stronger border // apart from login forms, which have stronger border
$strong-input-border-color: #c7c7c7; $strong-input-border-color: #c7c7c7;

View file

@ -21,11 +21,11 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event';
export default class Resend { export default class Resend {
static resendUnsentEvents(room) { static resendUnsentEvents(room) {
room.getPendingEvents().filter(function(ev) { return Promise.all(room.getPendingEvents().filter(function(ev) {
return ev.status === EventStatus.NOT_SENT; return ev.status === EventStatus.NOT_SENT;
}).forEach(function(event) { }).map(function(event) {
Resend.resend(event); return Resend.resend(event);
}); }));
} }
static cancelUnsentEvents(room) { static cancelUnsentEvents(room) {
@ -38,7 +38,7 @@ export default class Resend {
static resend(event) { static resend(event) {
const room = MatrixClientPeg.get().getRoom(event.getRoomId()); const room = MatrixClientPeg.get().getRoom(event.getRoomId());
MatrixClientPeg.get().resendEvent(event, room).then(function(res) { return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
dis.dispatch({ dis.dispatch({
action: 'message_sent', action: 'message_sent',
event: event, event: event,

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015-2020 The Matrix.org Foundation C.I.C. Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -20,16 +20,20 @@ import { _t, _td } from '../../languageHandler';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {EventStatus} from "matrix-js-sdk/src/models/event"; import {EventStatus} from "matrix-js-sdk/src/models/event";
import NotificationBadge from "../views/rooms/NotificationBadge";
import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState";
import AccessibleButton from "../views/elements/AccessibleButton";
import InlineSpinner from "../views/elements/InlineSpinner";
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2; const STATUS_BAR_EXPANDED_LARGE = 2;
function getUnsentMessages(room) { export function getUnsentMessages(room) {
if (!room) { return []; } if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) { return room.getPendingEvents().filter(function(ev) {
return ev.status === EventStatus.NOT_SENT; return ev.status === EventStatus.NOT_SENT;
@ -76,6 +80,7 @@ export default class RoomStatusBar extends React.Component {
syncState: MatrixClientPeg.get().getSyncState(), syncState: MatrixClientPeg.get().getSyncState(),
syncStateData: MatrixClientPeg.get().getSyncStateData(), syncStateData: MatrixClientPeg.get().getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room), unsentMessages: getUnsentMessages(this.props.room),
isResending: false,
}; };
componentDidMount() { componentDidMount() {
@ -109,7 +114,10 @@ export default class RoomStatusBar extends React.Component {
}; };
_onResendAllClick = () => { _onResendAllClick = () => {
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room).then(() => {
this.setState({isResending: false});
});
this.setState({isResending: true});
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
}; };
@ -120,10 +128,7 @@ export default class RoomStatusBar extends React.Component {
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
if (room.roomId !== this.props.room.roomId) return; if (room.roomId !== this.props.room.roomId) return;
this.setState({unsentMessages: getUnsentMessages(this.props.room)});
this.setState({
unsentMessages: getUnsentMessages(this.props.room),
});
}; };
// Check whether current size is greater than 0, if yes call props.onVisible // Check whether current size is greater than 0, if yes call props.onVisible
@ -141,7 +146,7 @@ export default class RoomStatusBar extends React.Component {
_getSize() { _getSize() {
if (this._shouldShowConnectionError()) { if (this._shouldShowConnectionError()) {
return STATUS_BAR_EXPANDED; return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0) { } else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
return STATUS_BAR_EXPANDED_LARGE; return STATUS_BAR_EXPANDED_LARGE;
} }
return STATUS_BAR_HIDDEN; return STATUS_BAR_HIDDEN;
@ -162,7 +167,6 @@ export default class RoomStatusBar extends React.Component {
_getUnsentMessageContent() { _getUnsentMessageContent() {
const unsentMessages = this.state.unsentMessages; const unsentMessages = this.state.unsentMessages;
if (!unsentMessages.length) return null;
let title; let title;
@ -206,75 +210,76 @@ export default class RoomStatusBar extends React.Component {
"Please <a>contact your service administrator</a> to continue using the service.", "Please <a>contact your service administrator</a> to continue using the service.",
), ),
}); });
} else if (
unsentMessages.length === 1 &&
unsentMessages[0].error &&
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error
) {
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} else { } else {
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); title = _t('Some of your messages have not been sent');
} }
const content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> " + let buttonRow = <>
"now. You can also select individual messages to resend or cancel.", <AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
{ count: unsentMessages.length }, {_t("Delete all")}
{ </AccessibleButton>
'resendText': (sub) => <AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>, {_t("Retry all")}
'cancelText': (sub) => </AccessibleButton>
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>, </>;
}, if (this.state.isResending) {
); buttonRow = <>
<InlineSpinner w={20} h={20} />
{/* span for css */}
<span>{_t("Sending")}</span>
</>;
}
return <div className="mx_RoomStatusBar_connectionLostBar"> return <>
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" /> <div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
<div> <div role="alert">
<div className="mx_RoomStatusBar_connectionLostBar_title"> <div className="mx_RoomStatusBar_unsentBadge">
{ title } <NotificationBadge
</div> notification={StaticNotificationState.RED_EXCLAMATION}
<div className="mx_RoomStatusBar_connectionLostBar_desc"> />
{ content } </div>
<div>
<div className="mx_RoomStatusBar_unsentTitle">
{ title }
</div>
<div className="mx_RoomStatusBar_unsentDescription">
{ _t("You can select all or individual messages to retry or delete") }
</div>
</div>
<div className="mx_RoomStatusBar_unsentButtonBar">
{buttonRow}
</div>
</div> </div>
</div> </div>
</div>; </>;
} }
// return suitable content for the main (text) part of the status bar. render() {
_getContent() {
if (this._shouldShowConnectionError()) { if (this._shouldShowConnectionError()) {
return ( return (
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar">
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title="/!\ " alt="/!\ " /> <div role="alert">
<div> <div className="mx_RoomStatusBar_connectionLostBar">
<div className="mx_RoomStatusBar_connectionLostBar_title"> <img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24"
{ _t('Connectivity to the server has been lost.') } height="24" title="/!\ " alt="/!\ " />
</div> <div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <div className="mx_RoomStatusBar_connectionLostBar_title">
{ _t('Sent messages will be stored until your connection has returned.') } {_t('Connectivity to the server has been lost.')}
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
{_t('Sent messages will be stored until your connection has returned.')}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
if (this.state.unsentMessages.length > 0) { if (this.state.unsentMessages.length > 0 || this.state.isResending) {
return this._getUnsentMessageContent(); return this._getUnsentMessageContent();
} }
return null; return null;
} }
render() {
const content = this._getContent();
return (
<div className="mx_RoomStatusBar">
<div role="alert">
{ content }
</div>
</div>
);
}
} }

View file

@ -1,8 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -34,7 +32,7 @@ import {MenuItem} from "../../structures/ContextMenu";
import {EventType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
function canCancel(eventStatus) { export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
} }
@ -98,21 +96,6 @@ export default class MessageContextMenu extends React.Component {
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
} }
onResendClick = () => {
Resend.resend(this.props.mxEvent);
this.closeMenu();
};
onResendEditClick = () => {
Resend.resend(this.props.mxEvent.replacingEvent());
this.closeMenu();
};
onResendRedactionClick = () => {
Resend.resend(this.props.mxEvent.localRedactionEvent());
this.closeMenu();
};
onResendReactionsClick = () => { onResendReactionsClick = () => {
for (const reaction of this._getUnsentReactions()) { for (const reaction of this._getUnsentReactions()) {
Resend.resend(reaction); Resend.resend(reaction);
@ -170,29 +153,6 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu(); this.closeMenu();
}; };
onCancelSendClick = () => {
const mxEvent = this.props.mxEvent;
const editEvent = mxEvent.replacingEvent();
const redactEvent = mxEvent.localRedactionEvent();
const pendingReactions = this._getPendingReactions();
if (editEvent && canCancel(editEvent.status)) {
Resend.removeFromQueue(editEvent);
}
if (redactEvent && canCancel(redactEvent.status)) {
Resend.removeFromQueue(redactEvent);
}
if (pendingReactions.length) {
for (const reaction of pendingReactions) {
Resend.removeFromQueue(reaction);
}
}
if (canCancel(mxEvent.status)) {
Resend.removeFromQueue(this.props.mxEvent);
}
this.closeMenu();
};
onForwardClick = () => { onForwardClick = () => {
if (this.props.onCloseDialog) this.props.onCloseDialog(); if (this.props.onCloseDialog) this.props.onCloseDialog();
dis.dispatch({ dis.dispatch({
@ -285,20 +245,9 @@ export default class MessageContextMenu extends React.Component {
const me = cli.getUserId(); const me = cli.getUserId();
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status; const eventStatus = mxEvent.status;
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
const unsentReactionsCount = this._getUnsentReactions().length; const unsentReactionsCount = this._getUnsentReactions().length;
const pendingReactionsCount = this._getPendingReactions().length;
const allowCancel = canCancel(mxEvent.status) ||
canCancel(editStatus) ||
canCancel(redactStatus) ||
pendingReactionsCount !== 0;
let resendButton;
let resendEditButton;
let resendReactionsButton; let resendReactionsButton;
let resendRedactionButton;
let redactButton; let redactButton;
let cancelButton;
let forwardButton; let forwardButton;
let pinButton; let pinButton;
let unhidePreviewButton; let unhidePreviewButton;
@ -309,22 +258,6 @@ export default class MessageContextMenu extends React.Component {
// status is SENT before remote-echo, null after // status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT; const isSent = !eventStatus || eventStatus === EventStatus.SENT;
if (!mxEvent.isRedacted()) { if (!mxEvent.isRedacted()) {
if (eventStatus === EventStatus.NOT_SENT) {
resendButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
{ _t('Resend') }
</MenuItem>
);
}
if (editStatus === EventStatus.NOT_SENT) {
resendEditButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendEditClick}>
{ _t('Resend edit') }
</MenuItem>
);
}
if (unsentReactionsCount !== 0) { if (unsentReactionsCount !== 0) {
resendReactionsButton = ( resendReactionsButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}> <MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
@ -334,14 +267,6 @@ export default class MessageContextMenu extends React.Component {
} }
} }
if (redactStatus === EventStatus.NOT_SENT) {
resendRedactionButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendRedactionClick}>
{ _t('Resend removal') }
</MenuItem>
);
}
if (isSent && this.state.canRedact) { if (isSent && this.state.canRedact) {
redactButton = ( redactButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}> <MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
@ -350,14 +275,6 @@ export default class MessageContextMenu extends React.Component {
); );
} }
if (allowCancel) {
cancelButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
{ _t('Cancel Sending') }
</MenuItem>
);
}
if (isContentActionable(mxEvent)) { if (isContentActionable(mxEvent)) {
forwardButton = ( forwardButton = (
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}> <MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
@ -455,12 +372,8 @@ export default class MessageContextMenu extends React.Component {
return ( return (
<div className="mx_MessageContextMenu"> <div className="mx_MessageContextMenu">
{ resendButton }
{ resendEditButton }
{ resendReactionsButton } { resendReactionsButton }
{ resendRedactionButton }
{ redactButton } { redactButton }
{ cancelButton }
{ forwardButton } { forwardButton }
{ pinButton } { pinButton }
{ viewSourceButton } { viewSourceButton }

View file

@ -160,7 +160,6 @@ export default class EditHistoryMessage extends React.PureComponent {
"mx_EventTile": true, "mx_EventTile": true,
// Note: we keep the `sending` state class for tests, not for our styles // Note: we keep the `sending` state class for tests, not for our styles
"mx_EventTile_sending": isSending, "mx_EventTile_sending": isSending,
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
}); });
return ( return (
<li> <li>

View file

@ -29,6 +29,8 @@ import RoomContext from "../../../contexts/RoomContext";
import Toolbar from "../../../accessibility/Toolbar"; import Toolbar from "../../../accessibility/Toolbar";
import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {canCancel} from "../context_menus/MessageContextMenu";
import Resend from "../../../Resend";
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -169,45 +171,118 @@ export default class MessageActionBar extends React.PureComponent {
}); });
}; };
render() { /**
let reactButton; * Runs a given fn on the set of possible events to test. The first event
let replyButton; * that passes the checkFn will have fn executed on it. Both functions take
let editButton; * a MatrixEvent object. If no particular conditions are needed, checkFn can
* be null/undefined. If no functions pass the checkFn, no action will be
* taken.
* @param {Function} fn The execution function.
* @param {Function} checkFn The test function.
*/
runActionOnFailedEv(fn, checkFn) {
if (!checkFn) checkFn = () => true;
if (isContentActionable(this.props.mxEvent)) { const mxEvent = this.props.mxEvent;
if (this.context.canReact) { const editEvent = mxEvent.replacingEvent();
reactButton = ( const redactEvent = mxEvent.localRedactionEvent();
<ReactButton mxEvent={this.props.mxEvent} reactions={this.props.reactions} onFocusChange={this.onFocusChange} /> const tryOrder = [redactEvent, editEvent, mxEvent];
); for (const ev of tryOrder) {
} if (ev && checkFn(ev)) {
if (this.context.canReply) { fn(ev);
replyButton = <RovingAccessibleTooltipButton break;
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
/>;
} }
} }
}
onResendClick = (ev) => {
this.runActionOnFailedEv((tarEv) => Resend.resend(tarEv));
};
onCancelClick = (ev) => {
this.runActionOnFailedEv(
(tarEv) => Resend.removeFromQueue(tarEv),
(testEv) => canCancel(testEv.status),
);
};
render() {
const toolbarOpts = [];
if (canEditContent(this.props.mxEvent)) { if (canEditContent(this.props.mxEvent)) {
editButton = <RovingAccessibleTooltipButton toolbarOpts.push(<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton" className="mx_MessageActionBar_maskButton mx_MessageActionBar_editButton"
title={_t("Edit")} title={_t("Edit")}
onClick={this.onEditClick} onClick={this.onEditClick}
/>; key="edit"
/>);
} }
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. const cancelSendingButton = <RovingAccessibleTooltipButton
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off"> className="mx_MessageActionBar_maskButton mx_MessageActionBar_cancelButton"
{reactButton} title={_t("Delete")}
{replyButton} onClick={this.onCancelClick}
{editButton} key="cancel"
<OptionsButton />;
// We show a different toolbar for failed events, so detect that first.
const mxEvent = this.props.mxEvent;
const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status;
const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status;
const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus);
const isFailed = [mxEvent.status, editStatus, redactStatus].includes("not_sent");
if (allowCancel && isFailed) {
// The resend button needs to appear ahead of the edit button, so insert to the
// start of the opts
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_resendButton"
title={_t("Retry")}
onClick={this.onResendClick}
key="resend"
/>);
// The delete button should appear last, so we can just drop it at the end
toolbarOpts.push(cancelSendingButton);
} else {
if (isContentActionable(this.props.mxEvent)) {
// Like the resend button, the react and reply buttons need to appear before the edit.
// The only catch is we do the reply button first so that we can make sure the react
// button is the very first button without having to do length checks for `splice()`.
if (this.context.canReply) {
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
key="reply"
/>);
}
if (this.context.canReact) {
toolbarOpts.splice(0, 0, <ReactButton
mxEvent={this.props.mxEvent}
reactions={this.props.reactions}
onFocusChange={this.onFocusChange}
key="react"
/>);
}
}
if (allowCancel) {
toolbarOpts.push(cancelSendingButton);
}
// The menu button should be last, so dump it there.
toolbarOpts.push(<OptionsButton
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
getReplyThread={this.props.getReplyThread} getReplyThread={this.props.getReplyThread}
getTile={this.props.getTile} getTile={this.props.getTile}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
onFocusChange={this.onFocusChange} onFocusChange={this.onFocusChange}
/> key="menu"
/>);
}
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
{toolbarOpts}
</Toolbar>; </Toolbar>;
} }
} }

View file

@ -40,6 +40,8 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStor
import {objectHasDiff} from "../../../utils/objects"; import {objectHasDiff} from "../../../utils/objects";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip"; import Tooltip from "../elements/Tooltip";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "./NotificationBadge";
const eventTileTypes = { const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent', [EventType.RoomMessage]: 'messages.MessageEvent',
@ -838,7 +840,6 @@ export default class EventTile extends React.Component {
mx_EventTile_12hr: this.props.isTwelveHour, mx_EventTile_12hr: this.props.isTwelveHour,
// Note: we keep the `sending` state class for tests, not for our styles // Note: we keep the `sending` state class for tests, not for our styles
mx_EventTile_sending: !isEditing && isSending, mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent',
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(), mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation, mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
@ -1253,11 +1254,19 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
render() { render() {
const isSent = !this.props.messageState || this.props.messageState === 'sent'; const isSent = !this.props.messageState || this.props.messageState === 'sent';
const isFailed = this.props.messageState === 'not_sent';
const receiptClasses = classNames({ const receiptClasses = classNames({
'mx_EventTile_receiptSent': isSent, 'mx_EventTile_receiptSent': isSent,
'mx_EventTile_receiptSending': !isSent, 'mx_EventTile_receiptSending': !isSent && !isFailed,
}); });
let nonCssBadge = null;
if (isFailed) {
nonCssBadge = <NotificationBadge
notification={StaticNotificationState.RED_EXCLAMATION}
/>;
}
let tooltip = null; let tooltip = null;
if (this.state.hover) { if (this.state.hover) {
let label = _t("Sending your message..."); let label = _t("Sending your message...");
@ -1265,6 +1274,8 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
label = _t("Encrypting your message..."); label = _t("Encrypting your message...");
} else if (isSent) { } else if (isSent) {
label = _t("Your message was sent"); label = _t("Your message was sent");
} else if (isFailed) {
label = _t("Failed to send");
} }
// The yOffset is somewhat arbitrary - it just brings the tooltip down to be more associated // The yOffset is somewhat arbitrary - it just brings the tooltip down to be more associated
// with the read receipt. // with the read receipt.
@ -1273,6 +1284,7 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
return <span className="mx_EventTile_readAvatars"> return <span className="mx_EventTile_readAvatars">
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}> <span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
{nonCssBadge}
{tooltip} {tooltip}
</span> </span>
</span>; </span>;

View file

@ -30,7 +30,7 @@ interface IProps {
* If true, the badge will show a count if at all possible. This is typically * If true, the badge will show a count if at all possible. This is typically
* used to override the user's preference for things like room sublists. * used to override the user's preference for things like room sublists.
*/ */
forceCount: boolean; forceCount?: boolean;
/** /**
* The room ID, if any, the badge represents. * The room ID, if any, the badge represents.

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015-2018, 2020, 2021 The Matrix.org Foundation C.I.C.
Copyright 2017, 2018 Vector Creations Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -37,7 +35,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar"; import GroupAvatar from "../avatars/GroupAvatar";
import ExtraTile from "./ExtraTile"; import ExtraTile from "./ExtraTile";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -492,7 +489,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
isSelected={false} isSelected={false}
displayName={g.name} displayName={g.name}
avatar={avatar} avatar={avatar}
notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)} notificationState={StaticNotificationState.RED_EXCLAMATION}
onClick={openGroup} onClick={openGroup}
key={`temporaryGroupTile_${g.groupId}`} key={`temporaryGroupTile_${g.groupId}`}
/> />

View file

@ -1,8 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2015-2017, 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -19,6 +17,7 @@ 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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
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";
@ -51,7 +50,9 @@ import IconizedContextMenu, {
IconizedContextMenuRadio, IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu"; } from "../context_menus/IconizedContextMenu";
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { getUnsentMessages } from "../../structures/RoomStatusBar";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
interface IProps { interface IProps {
room: Room; room: Room;
@ -67,6 +68,7 @@ interface IState {
notificationsMenuPosition: PartialDOMRect; notificationsMenuPosition: PartialDOMRect;
generalMenuPosition: PartialDOMRect; generalMenuPosition: PartialDOMRect;
messagePreview?: string; messagePreview?: string;
hasUnsentEvents: boolean;
} }
const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`; const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
@ -93,6 +95,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
notificationsMenuPosition: null, notificationsMenuPosition: null,
generalMenuPosition: null, generalMenuPosition: null,
hasUnsentEvents: this.countUnsentEvents() > 0,
// generatePreview() will return nothing if the user has previews disabled // generatePreview() will return nothing if the user has previews disabled
messagePreview: this.generatePreview(), messagePreview: this.generatePreview(),
@ -101,6 +104,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.roomProps = EchoChamber.forRoom(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room);
} }
private countUnsentEvents(): number {
return getUnsentMessages(this.props.room).length;
}
private onRoomNameUpdate = (room) => { private onRoomNameUpdate = (room) => {
this.forceUpdate(); this.forceUpdate();
} }
@ -109,6 +116,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.forceUpdate(); // notification state changed - update this.forceUpdate(); // notification state changed - update
}; };
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
if (!room?.roomId === this.props.room.roomId) return;
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
};
private onRoomPropertyUpdate = (property: CachedRoomKey) => { private onRoomPropertyUpdate = (property: CachedRoomKey) => {
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate(); if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
// else ignore - not important for this tile // else ignore - not important for this tile
@ -167,6 +179,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate, this.onCommunityUpdate,
); );
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
} }
public componentWillUnmount() { public componentWillUnmount() {
@ -191,6 +204,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate, this.onCommunityUpdate,
); );
MatrixClientPeg.get()?.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
} }
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
@ -554,17 +568,30 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
/>; />;
let badge: React.ReactNode; let badge: React.ReactNode;
if (!this.props.isMinimized && this.notificationState) { if (!this.props.isMinimized) {
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
badge = ( if (this.state.hasUnsentEvents) {
<div className="mx_RoomTile_badgeContainer" aria-hidden="true"> // hardcode the badge to a danger state when there's unsent messages
<NotificationBadge badge = (
notification={this.notificationState} <div className="mx_RoomTile_badgeContainer" aria-hidden="true">
forceCount={false} <NotificationBadge
roomId={this.props.room.roomId} notification={StaticNotificationState.RED_EXCLAMATION}
/> forceCount={false}
</div> roomId={this.props.room.roomId}
); />
</div>
);
} else if (this.notificationState) {
badge = (
<div className="mx_RoomTile_badgeContainer" aria-hidden="true">
<NotificationBadge
notification={this.notificationState}
forceCount={false}
roomId={this.props.room.roomId}
/>
</div>
);
}
} }
let messagePreview = null; let messagePreview = null;

View file

@ -658,7 +658,6 @@
"No homeserver URL provided": "No homeserver URL provided", "No homeserver URL provided": "No homeserver URL provided",
"Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration",
"Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration",
"The message you are trying to send is too large.": "The message you are trying to send is too large.",
"This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.",
"This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.", "This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.",
"This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
@ -1453,6 +1452,7 @@
"Sending your message...": "Sending your message...", "Sending your message...": "Sending your message...",
"Encrypting your message...": "Encrypting your message...", "Encrypting your message...": "Encrypting your message...",
"Your message was sent": "Your message was sent", "Your message was sent": "Your message was sent",
"Failed to send": "Failed to send",
"Please select the destination room for this message": "Please select the destination room for this message", "Please select the destination room for this message": "Please select the destination room for this message",
"Scroll to most recent messages": "Scroll to most recent messages", "Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview", "Close preview": "Close preview",
@ -1811,8 +1811,9 @@
"The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.", "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.",
"Error decrypting audio": "Error decrypting audio", "Error decrypting audio": "Error decrypting audio",
"React": "React", "React": "React",
"Reply": "Reply",
"Edit": "Edit", "Edit": "Edit",
"Retry": "Retry",
"Reply": "Reply",
"Message Actions": "Message Actions", "Message Actions": "Message Actions",
"Attachment": "Attachment", "Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment", "Error decrypting attachment": "Error decrypting attachment",
@ -2397,7 +2398,6 @@
"Confirm encryption setup": "Confirm encryption setup", "Confirm encryption setup": "Confirm encryption setup",
"Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.",
"Unable to set up keys": "Unable to set up keys", "Unable to set up keys": "Unable to set up keys",
"Retry": "Retry",
"Restoring keys from backup": "Restoring keys from backup", "Restoring keys from backup": "Restoring keys from backup",
"Fetching keys from server...": "Fetching keys from server...", "Fetching keys from server...": "Fetching keys from server...",
"%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored",
@ -2426,10 +2426,7 @@
"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 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",
"Cancel Sending": "Cancel Sending",
"Forward Message": "Forward Message", "Forward Message": "Forward Message",
"Pin Message": "Pin Message", "Pin Message": "Pin Message",
"Unhide Preview": "Unhide Preview", "Unhide Preview": "Unhide Preview",
@ -2617,10 +2614,11 @@
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
"Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.", "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
"%(count)s of your messages have not been sent.|other": "Some of your messages have not been sent.", "Some of your messages have not been sent": "Some of your messages have not been sent",
"%(count)s of your messages have not been sent.|one": "Your message was not sent.", "Delete all": "Delete all",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.", "Retry all": "Retry all",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.", "Sending": "Sending",
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",

View file

@ -18,6 +18,8 @@ import { NotificationColor } from "./NotificationColor";
import { NotificationState } from "./NotificationState"; import { NotificationState } from "./NotificationState";
export class StaticNotificationState extends NotificationState { export class StaticNotificationState extends NotificationState {
public static readonly RED_EXCLAMATION = StaticNotificationState.forSymbol("!", NotificationColor.Red);
constructor(symbol: string, count: number, color: NotificationColor) { constructor(symbol: string, count: number, color: NotificationColor) {
super(); super();
this._symbol = symbol; this._symbol = symbol;

View file

@ -49,12 +49,6 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e
} }
} }
export function messageForSendError(errorData) {
if (errorData.errcode === "M_TOO_LARGE") {
return _t("The message you are trying to send is too large.");
}
}
export function messageForSyncError(err) { export function messageForSyncError(err) {
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const limitError = messageForResourceLimitError( const limitError = messageForResourceLimitError(

View file

@ -29,7 +29,10 @@ function waitForRoomListStoreUpdate() {
describe('RoomList', () => { describe('RoomList', () => {
function createRoom(opts) { function createRoom(opts) {
const room = new Room(generateRoomId(), null, client.getUserId()); const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
// The room list now uses getPendingEvents(), so we need a detached ordering.
pendingEventOrdering: "detached",
});
if (opts) { if (opts) {
Object.assign(room, opts); Object.assign(room, opts);
} }

View file

@ -79,6 +79,13 @@ export function createTestClient() {
generateClientSecret: () => "t35tcl1Ent5ECr3T", generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: () => false, isGuest: () => false,
isCryptoEnabled: () => false, isCryptoEnabled: () => false,
// Used by various internal bits we aren't concerned with (yet)
_sessionStore: {
store: {
getItem: jest.fn(),
},
},
}; };
} }