Merge remote-tracking branch 'origin/develop' into release-v1.6.2

pull/21833/head
David Baker 2019-10-04 10:17:13 +01:00
commit a79dce1623
37 changed files with 489 additions and 195 deletions

View File

@ -87,6 +87,7 @@
@import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_Dropdown.scss"; @import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";

View File

@ -0,0 +1,34 @@
/*
Copyright 2019 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_ErrorBoundary {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.mx_ErrorBoundary_body {
display: flex;
flex-direction: column;
max-width: 400px;
align-items: center;
.mx_AccessibleButton {
margin-top: 5px;
}
}

View File

@ -59,3 +59,36 @@ limitations under the License.
color: $imagebody-giflabel-color; color: $imagebody-giflabel-color;
pointer-events: none; pointer-events: none;
} }
.mx_HiddenImagePlaceholder {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
// To center the text in the middle of the frame
display: flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
background-color: $header-panel-bg-color;
.mx_HiddenImagePlaceholder_button {
color: $accent-color;
img {
margin-right: 8px;
}
span {
vertical-align: text-bottom;
}
}
}
.mx_EventTile:hover .mx_HiddenImagePlaceholder {
background-color: $primary-bg-color;
}

View File

@ -22,3 +22,14 @@ limitations under the License.
position: absolute; position: absolute;
top: 50%; top: 50%;
} }
.mx_MStickerBody_hidden {
max-width: 220px;
text-decoration: none;
text-align: center;
// To center the text in the middle of the frame
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -42,7 +42,7 @@ limitations under the License.
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
outline: none; outline: none;
overflow-x: auto; overflow-x: hidden;
span.mx_UserPill, span.mx_RoomPill { span.mx_UserPill, span.mx_RoomPill {
padding-left: 21px; padding-left: 21px;

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="14" viewBox="0 0 18 14">
<g fill="none" fill-rule="evenodd" stroke="#03B381" stroke-linecap="round" stroke-linejoin="round" transform="translate(1 1)">
<path d="M0 6s3-6 8.25-6 8.25 6 8.25 6-3 6-8.25 6S0 6 0 6z"/>
<circle cx="8.25" cy="6" r="2.25"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -544,6 +544,9 @@ export function softLogout() {
// been soft logged out, despite having credentials and data for a MatrixClient). // been soft logged out, despite having credentials and data for a MatrixClient).
localStorage.setItem("mx_soft_logout", "true"); localStorage.setItem("mx_soft_logout", "true");
// Dev note: please keep this log line around. It can be useful for track down
// random clients stopping in the middle of the logs.
console.log("Soft logout initiated");
_isLoggingOut = true; // to avoid repeated flags _isLoggingOut = true; // to avoid repeated flags
stopMatrixClient(/*unsetClient=*/false); stopMatrixClient(/*unsetClient=*/false);
dis.dispatch({action: 'on_client_not_viable'}); // generic version of on_logged_out dis.dispatch({action: 'on_client_not_viable'}); // generic version of on_logged_out

View File

@ -126,11 +126,12 @@ const FilePanel = createReactClass({
tileShape="file_grid" tileShape="file_grid"
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
empty={_t('There are no visible files in this room')} empty={_t('There are no visible files in this room')}
role="tabpanel"
/> />
); );
} else { } else {
return ( return (
<div className="mx_FilePanel"> <div className="mx_FilePanel" role="tabpanel">
<Loader /> <Loader />
</div> </div>
); );

View File

@ -52,8 +52,10 @@ const LeftPanel = createReactClass({
componentWillMount: function() { componentWillMount: function() {
this.focusedElement = null; this.focusedElement = null;
this._settingWatchRef = SettingsStore.watchSetting( this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
"breadcrumbs", null, this._onBreadcrumbsChanged); "breadcrumbs", null, this._onBreadcrumbsChanged);
this._tagPanelWatcherRef = SettingsStore.watchSetting(
"TagPanel.enableTagPanel", null, () => this.forceUpdate());
const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs"); const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs");
Analytics.setBreadcrumbs(useBreadcrumbs); Analytics.setBreadcrumbs(useBreadcrumbs);
@ -61,7 +63,8 @@ const LeftPanel = createReactClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
SettingsStore.unwatchSetting(this._settingWatchRef); SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef);
SettingsStore.unwatchSetting(this._tagPanelWatcherRef);
}, },
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate: function(nextProps, nextState) {

View File

@ -401,6 +401,12 @@ const LoggedInView = createReactClass({
const isClickShortcut = ev.target !== document.body && const isClickShortcut = ev.target !== document.body &&
(ev.key === "Space" || ev.key === "Enter"); (ev.key === "Space" || ev.key === "Enter");
// XXX: Remove after CIDER replaces Slate completely: https://github.com/vector-im/riot-web/issues/11036
if (ev.key === "Backspace") {
ev.stopPropagation();
return;
}
if (!isClickShortcut && !canElementReceiveInput(ev.target)) { if (!isClickShortcut && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input // synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true); dis.dispatch({action: 'focus_composer'}, true);

View File

@ -1808,28 +1808,26 @@ export default createReactClass({
render: function() { render: function() {
// console.log(`Rendering MatrixChat with view ${this.state.view}`); // console.log(`Rendering MatrixChat with view ${this.state.view}`);
let view;
if ( if (
this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOADING ||
this.state.view === VIEWS.LOGGING_IN this.state.view === VIEWS.LOGGING_IN
) { ) {
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
return ( view = (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
<Spinner /> <Spinner />
</div> </div>
); );
} } else if (this.state.view === VIEWS.POST_REGISTRATION) {
// needs to be before normal PageTypes as you are logged in technically // needs to be before normal PageTypes as you are logged in technically
if (this.state.view === VIEWS.POST_REGISTRATION) {
const PostRegistration = sdk.getComponent('structures.auth.PostRegistration'); const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
return ( view = (
<PostRegistration <PostRegistration
onComplete={this.onFinishPostRegistration} /> onComplete={this.onFinishPostRegistration} />
); );
} } else if (this.state.view === VIEWS.LOGGED_IN) {
if (this.state.view === VIEWS.LOGGED_IN) {
// store errors stop the client syncing and require user intervention, so we'll // store errors stop the client syncing and require user intervention, so we'll
// be showing a dialog. Don't show anything else. // be showing a dialog. Don't show anything else.
const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError; const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError;
@ -1843,7 +1841,7 @@ export default createReactClass({
* as using something like redux to avoid having a billion bits of state kicking around. * as using something like redux to avoid having a billion bits of state kicking around.
*/ */
const LoggedInView = sdk.getComponent('structures.LoggedInView'); const LoggedInView = sdk.getComponent('structures.LoggedInView');
return ( view = (
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()} <LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated} onRoomCreated={this.onRoomCreated}
onCloseAllSettings={this.onCloseAllSettings} onCloseAllSettings={this.onCloseAllSettings}
@ -1863,26 +1861,22 @@ export default createReactClass({
{messageForSyncError(this.state.syncError)} {messageForSyncError(this.state.syncError)}
</div>; </div>;
} }
return ( view = (
<div className="mx_MatrixChat_splash"> <div className="mx_MatrixChat_splash">
{errorBox} {errorBox}
<Spinner /> <Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}> <a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
{ _t('Logout') } {_t('Logout')}
</a> </a>
</div> </div>
); );
} }
} } else if (this.state.view === VIEWS.WELCOME) {
if (this.state.view === VIEWS.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome'); const Welcome = sdk.getComponent('auth.Welcome');
return <Welcome />; view = <Welcome />;
} } else if (this.state.view === VIEWS.REGISTER) {
if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.auth.Registration'); const Registration = sdk.getComponent('structures.auth.Registration');
return ( view = (
<Registration <Registration
clientSecret={this.state.register_client_secret} clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id} sessionId={this.state.register_session_id}
@ -1896,12 +1890,9 @@ export default createReactClass({
{...this.getServerProperties()} {...this.getServerProperties()}
/> />
); );
} } else if (this.state.view === VIEWS.FORGOT_PASSWORD) {
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
return ( view = (
<ForgotPassword <ForgotPassword
onComplete={this.onLoginClick} onComplete={this.onLoginClick}
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
@ -1909,11 +1900,9 @@ export default createReactClass({
{...this.getServerProperties()} {...this.getServerProperties()}
/> />
); );
} } else if (this.state.view === VIEWS.LOGIN) {
if (this.state.view === VIEWS.LOGIN) {
const Login = sdk.getComponent('structures.auth.Login'); const Login = sdk.getComponent('structures.auth.Login');
return ( view = (
<Login <Login
onLoggedIn={Lifecycle.setLoggedIn} onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
@ -1924,18 +1913,21 @@ export default createReactClass({
{...this.getServerProperties()} {...this.getServerProperties()}
/> />
); );
} } else if (this.state.view === VIEWS.SOFT_LOGOUT) {
if (this.state.view === VIEWS.SOFT_LOGOUT) {
const SoftLogout = sdk.getComponent('structures.auth.SoftLogout'); const SoftLogout = sdk.getComponent('structures.auth.SoftLogout');
return ( view = (
<SoftLogout <SoftLogout
realQueryParams={this.props.realQueryParams} realQueryParams={this.props.realQueryParams}
onTokenLoginCompleted={this.props.onTokenLoginCompleted} onTokenLoginCompleted={this.props.onTokenLoginCompleted}
/> />
); );
} else {
console.error(`Unknown view ${this.state.view}`);
} }
console.error(`Unknown view ${this.state.view}`); const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
return <ErrorBoundary>
{view}
</ErrorBoundary>;
}, },
}); });

View File

@ -46,12 +46,13 @@ const NotificationPanel = createReactClass({
showUrlPreview={false} showUrlPreview={false}
tileShape="notif" tileShape="notif"
empty={_t('You have no visible notifications')} empty={_t('You have no visible notifications')}
role="tabpanel"
/> />
); );
} else { } else {
console.error("No notifTimelineSet available!"); console.error("No notifTimelineSet available!");
return ( return (
<div className="mx_NotificationPanel"> <div className="mx_NotificationPanel" role="tabpanel">
<Loader /> <Loader />
</div> </div>
); );

View File

@ -258,7 +258,7 @@ const RoomSubList = createReactClass({
const tabindex = this.props.isFiltered ? "0" : "-1"; const tabindex = this.props.isFiltered ? "0" : "-1";
return ( return (
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header"> <div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}> <AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex} aria-expanded={!isCollapsed}>
{ chevron } { chevron }
<span>{this.props.label}</span> <span>{this.props.label}</span>
{ incomingCall } { incomingCall }

View File

@ -1417,7 +1417,8 @@ module.exports = createReactClass({
const scrollState = messagePanel.getScrollState(); const scrollState = messagePanel.getScrollState();
if (scrollState.stuckAtBottom) { // getScrollState on TimelinePanel *may* return null, so guard against that
if (!scrollState || scrollState.stuckAtBottom) {
// we don't really expect to be in this state, but it will // we don't really expect to be in this state, but it will
// occasionally happen when no scroll state has been set on the // occasionally happen when no scroll state has been set on the
// messagePanel (ie, we didn't have an initial event (so it's // messagePanel (ie, we didn't have an initial event (so it's
@ -1566,12 +1567,14 @@ module.exports = createReactClass({
const TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar"); const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder"); const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) { if (!this.state.room) {
const loading = this.state.roomLoading || this.state.peekLoading; const loading = this.state.roomLoading || this.state.peekLoading;
if (loading) { if (loading) {
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar <RoomPreviewBar
canPreview={false} canPreview={false}
previewLoading={this.state.peekLoading} previewLoading={this.state.peekLoading}
@ -1580,6 +1583,7 @@ module.exports = createReactClass({
joining={this.state.joining} joining={this.state.joining}
oobData={this.props.oobData} oobData={this.props.oobData}
/> />
</ErrorBoundary>
</div> </div>
); );
} else { } else {
@ -1597,6 +1601,7 @@ module.exports = createReactClass({
const roomAlias = this.state.roomAlias; const roomAlias = this.state.roomAlias;
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
@ -1609,6 +1614,7 @@ module.exports = createReactClass({
signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null} signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
room={this.state.room} room={this.state.room}
/> />
</ErrorBoundary>
</div> </div>
); );
} }
@ -1618,12 +1624,14 @@ module.exports = createReactClass({
if (myMembership == 'invite') { if (myMembership == 'invite') {
if (this.state.joining || this.state.rejecting) { if (this.state.joining || this.state.rejecting) {
return ( return (
<ErrorBoundary>
<RoomPreviewBar <RoomPreviewBar
canPreview={false} canPreview={false}
error={this.state.roomLoadError} error={this.state.roomLoadError}
joining={this.state.joining} joining={this.state.joining}
rejecting={this.state.rejecting} rejecting={this.state.rejecting}
/> />
</ErrorBoundary>
); );
} else { } else {
const myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
@ -1638,6 +1646,7 @@ module.exports = createReactClass({
// We have a regular invite for this room. // We have a regular invite for this room.
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick} onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked} onRejectClick={this.onRejectButtonClicked}
@ -1646,6 +1655,7 @@ module.exports = createReactClass({
joining={this.state.joining} joining={this.state.joining}
room={this.state.room} room={this.state.room}
/> />
</ErrorBoundary>
</div> </div>
); );
} }
@ -1942,6 +1952,7 @@ module.exports = createReactClass({
return ( return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView"> <main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<ErrorBoundary>
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData} oobData={this.props.oobData}
inRoom={myMembership === 'join'} inRoom={myMembership === 'join'}
@ -1960,23 +1971,24 @@ module.exports = createReactClass({
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
> >
<div className={fadableSectionClasses}> <div className={fadableSectionClasses}>
{ auxPanel } {auxPanel}
<div className="mx_RoomView_timeline"> <div className="mx_RoomView_timeline">
{ topUnreadMessagesBar } {topUnreadMessagesBar}
{ jumpToBottom } {jumpToBottom}
{ messagePanel } {messagePanel}
{ searchResultsPanel } {searchResultsPanel}
</div> </div>
<div className={statusBarAreaClass}> <div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox"> <div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div> <div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar } {statusBar}
</div> </div>
</div> </div>
{ previewBar } {previewBar}
{ messageComposer } {messageComposer}
</div> </div>
</MainSplit> </MainSplit>
</ErrorBoundary>
</main> </main>
); );
}, },

View File

@ -1,18 +1,19 @@
/* /*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -55,7 +56,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
label={title} label={title}
/> : <div />; /> : <div />;
return ( return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}> <AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
{ tip } { tip }
</AccessibleButton> </AccessibleButton>
); );

View File

@ -0,0 +1,104 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
export default class ErrorBoundary extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
componentDidCatch(error, { componentStack }) {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
console.error(
"The above error occured while React was rendering the following components:",
componentStack,
);
}
_onClearCacheAndReload = () => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => {
PlatformPeg.get().reload();
});
};
_onBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
};
render() {
if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
<p>{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.", {}, {
newIssueLink: (sub) => {
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
},
},
)}</p>
<p>{_t(
"If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}</p>
<AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")}
</AccessibleButton>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
</div>;
}
return this.props.children;
}
}

View File

@ -95,6 +95,8 @@ export default class InteractiveTooltip extends React.Component {
content: PropTypes.node.isRequired, content: PropTypes.node.isRequired,
// Function to call when visibility of the tooltip changes // Function to call when visibility of the tooltip changes
onVisibilityChange: PropTypes.func, onVisibilityChange: PropTypes.func,
// flag to forcefully hide this tooltip
forceHidden: PropTypes.bool,
}; };
constructor() { constructor() {
@ -269,8 +271,8 @@ export default class InteractiveTooltip extends React.Component {
renderTooltip() { renderTooltip() {
const { contentRect, visible } = this.state; const { contentRect, visible } = this.state;
if (!visible) { if (this.props.forceHidden === true || !visible) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer()); ReactDOM.render(null, getOrCreateContainer());
return null; return null;
} }

View File

@ -183,7 +183,7 @@ module.exports = createReactClass({
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper'); const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo" role="tabpanel">
<GeminiScrollbarWrapper autoshow={true}> <GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}> <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" /> <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />

View File

@ -222,7 +222,7 @@ export default createReactClass({
} }
return ( return (
<div className="mx_MemberList"> <div className="mx_MemberList" role="tabpanel">
{ inviteButton } { inviteButton }
<GeminiScrollbarWrapper autoshow={true}> <GeminiScrollbarWrapper autoshow={true}>
{ joined } { joined }

View File

@ -214,7 +214,7 @@ module.exports = createReactClass({
const groupRoomName = this.state.groupRoom.displayname; const groupRoomName = this.state.groupRoom.displayname;
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo" role="tabpanel">
<GeminiScrollbarWrapper autoshow={true}> <GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}> <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" /> <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />

View File

@ -153,7 +153,7 @@ export default createReactClass({
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const TruncatedList = sdk.getComponent("elements.TruncatedList"); const TruncatedList = sdk.getComponent("elements.TruncatedList");
return ( return (
<div className="mx_GroupRoomList"> <div className="mx_GroupRoomList" role="tabpanel">
{ inviteButton } { inviteButton }
<GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper"> <GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt} <TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}

View File

@ -64,6 +64,7 @@ export default class MImageBody extends React.Component {
imgLoaded: false, imgLoaded: false,
loadedImageDimensions: null, loadedImageDimensions: null,
hover: false, hover: false,
showImage: SettingsStore.getValue("showImages"),
}; };
} }
@ -86,9 +87,19 @@ export default class MImageBody extends React.Component {
} }
} }
showImage() {
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
this.setState({showImage: true});
}
onClick(ev) { onClick(ev) {
if (ev.button === 0 && !ev.metaKey) { if (ev.button === 0 && !ev.metaKey) {
ev.preventDefault(); ev.preventDefault();
if (!this.state.showImage) {
this.showImage();
return;
}
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const httpUrl = this._getContentUrl(); const httpUrl = this._getContentUrl();
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
@ -120,7 +131,7 @@ export default class MImageBody extends React.Component {
onImageEnter(e) { onImageEnter(e) {
this.setState({ hover: true }); this.setState({ hover: true });
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target;
@ -130,7 +141,7 @@ export default class MImageBody extends React.Component {
onImageLeave(e) { onImageLeave(e) {
this.setState({ hover: false }); this.setState({ hover: false });
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target;
@ -280,6 +291,12 @@ export default class MImageBody extends React.Component {
}); });
}).done(); }).done();
} }
// Remember that the user wanted to show this particular image
if (!this.state.showImage && localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true") {
this.setState({showImage: true});
}
this._afterComponentDidMount(); this._afterComponentDidMount();
} }
@ -321,14 +338,20 @@ export default class MImageBody extends React.Component {
// By doing this, the image "pops" into the timeline, but is still restricted // By doing this, the image "pops" into the timeline, but is still restricted
// by the same width and height logic below. // by the same width and height logic below.
if (!this.state.loadedImageDimensions) { if (!this.state.loadedImageDimensions) {
return this.wrapImage(contentUrl, let imageElement;
if (!this.state.showImage) {
imageElement = <HiddenImagePlaceholder />;
} else {
imageElement = (
<img style={{display: 'none'}} src={thumbUrl} ref="image" <img style={{display: 'none'}} src={thumbUrl} ref="image"
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
/>, />
); );
} }
return this.wrapImage(contentUrl, imageElement);
}
infoWidth = this.state.loadedImageDimensions.naturalWidth; infoWidth = this.state.loadedImageDimensions.naturalWidth;
infoHeight = this.state.loadedImageDimensions.naturalHeight; infoHeight = this.state.loadedImageDimensions.naturalHeight;
} }
@ -356,19 +379,26 @@ export default class MImageBody extends React.Component {
placeholder = this.getPlaceholder(); placeholder = this.getPlaceholder();
} }
const showPlaceholder = Boolean(placeholder); let showPlaceholder = Boolean(placeholder);
if (thumbUrl && !this.state.imgError) { if (thumbUrl && !this.state.imgError) {
// Restrict the width of the thumbnail here, otherwise it will fill the container // Restrict the width of the thumbnail here, otherwise it will fill the container
// which has the same width as the timeline // which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size // mx_MImageBody_thumbnail resizes img to exactly container size
img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image" img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
style={{ maxWidth: maxWidth + "px" }} style={{ maxWidth: maxWidth + "px" }}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter} onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />; onMouseLeave={this.onImageLeave} />
);
}
if (!this.state.showImage) {
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
} }
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
@ -454,3 +484,22 @@ export default class MImageBody extends React.Component {
</span>; </span>;
} }
} }
export class HiddenImagePlaceholder extends React.PureComponent {
static propTypes = {
hover: PropTypes.bool,
};
render() {
let className = 'mx_HiddenImagePlaceholder';
if (this.props.hover) className += ' mx_HiddenImagePlaceholder_hover';
return (
<div className={className}>
<div className='mx_HiddenImagePlaceholder_button'>
<img src={require("../../../../res/img/feather-customised/eye.svg")} width={17} height={12} />
<span>{_t("Show image")}</span>
</div>
</div>
);
}
}

View File

@ -14,21 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import MImageBody from './MImageBody'; import MImageBody from './MImageBody';
import sdk from '../../../index'; import sdk from '../../../index';
export default class MStickerBody extends MImageBody { export default class MStickerBody extends MImageBody {
// Empty to prevent default behaviour of MImageBody // Mostly empty to prevent default behaviour of MImageBody
onClick() { onClick(ev) {
ev.preventDefault();
if (!this.state.showImage) {
this.showImage();
}
} }
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding // MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
// which is added by mx_MStickerBody_wrapper // which is added by mx_MStickerBody_wrapper
wrapImage(contentUrl, children) { wrapImage(contentUrl, children) {
return <div className="mx_MStickerBody_wrapper"> { children } </div>; let onClick = null;
if (!this.state.showImage) {
onClick = this.onClick;
}
return <div className="mx_MStickerBody_wrapper" onClick={onClick}> { children } </div>;
} }
// Placeholder to show in place of the sticker image if // Placeholder to show in place of the sticker image if

View File

@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 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.
@ -42,8 +43,8 @@ export default class HeaderButton extends React.Component {
}); });
return <AccessibleButton return <AccessibleButton
aria-label={this.props.title} aria-selected={this.props.isHighlighted}
aria-expanded={this.props.isHighlighted} role="tab"
title={this.props.title} title={this.props.title}
className={classes} className={classes}
onClick={this.onClick}> onClick={this.onClick}>

View File

@ -91,7 +91,7 @@ export default class HeaderButtons extends React.Component {
render() { render() {
// inline style as this will be swapped around in future commits // inline style as this will be swapped around in future commits
return <div className="mx_HeaderButtons"> return <div className="mx_HeaderButtons" role="tablist">
{ this.renderButtons() } { this.renderButtons() }
</div>; </div>;
} }

View File

@ -18,6 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import { linkifyElement } from '../../../HtmlUtils'; import { linkifyElement } from '../../../HtmlUtils';
import SettingsStore from "../../../settings/SettingsStore";
const sdk = require('../../../index'); const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
@ -102,6 +103,9 @@ module.exports = createReactClass({
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
let image = p["og:image"]; let image = p["og:image"];
if (!SettingsStore.getValue("showImages")) {
image = null; // Don't render a button to show the image, just hide it outright
}
const imageMaxWidth = 100; const imageMaxHeight = 100; const imageMaxWidth = 100; const imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) { if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);

View File

@ -1124,7 +1124,7 @@ module.exports = createReactClass({
} }
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo" role="tabpanel">
<div className="mx_MemberInfo_name"> <div className="mx_MemberInfo_name">
{ backButton } { backButton }
{ e2eIconElement } { e2eIconElement }

View File

@ -475,7 +475,7 @@ module.exports = createReactClass({
} }
return ( return (
<div className="mx_MemberList"> <div className="mx_MemberList" role="tabpanel">
{ inviteButton } { inviteButton }
<AutoHideScrollbar> <AutoHideScrollbar>
<div className="mx_MemberList_wrapper"> <div className="mx_MemberList_wrapper">

View File

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import classNames from 'classnames';
export default class MessageComposerFormatBar extends React.PureComponent { export default class MessageComposerFormatBar extends React.PureComponent {
static propTypes = { static propTypes = {
@ -26,18 +26,26 @@ export default class MessageComposerFormatBar extends React.PureComponent {
shortcuts: PropTypes.object.isRequired, shortcuts: PropTypes.object.isRequired,
} }
constructor(props) {
super(props);
this.state = {visible: false};
}
render() { render() {
return (<div className="mx_MessageComposerFormatBar" ref={ref => this._formatBarRef = ref}> const classes = classNames("mx_MessageComposerFormatBar", {
<FormatButton shortcut={this.props.shortcuts.bold} label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" /> "mx_MessageComposerFormatBar_shown": this.state.visible,
<FormatButton shortcut={this.props.shortcuts.italics} label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" /> });
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" /> return (<div className={classes} ref={ref => this._formatBarRef = ref}>
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" /> <FormatButton label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
<FormatButton shortcut={this.props.shortcuts.quote} label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" /> <FormatButton label={_t("Italics")} onClick={() => this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} />
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" visible={this.state.visible} />
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
</div>); </div>);
} }
showAt(selectionRect) { showAt(selectionRect) {
this._formatBarRef.classList.add("mx_MessageComposerFormatBar_shown"); this.setState({visible: true});
const parentRect = this._formatBarRef.parentElement.getBoundingClientRect(); const parentRect = this._formatBarRef.parentElement.getBoundingClientRect();
this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`; this._formatBarRef.style.left = `${selectionRect.left - parentRect.left}px`;
// 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok. // 12 is half the height of the bar (e.g. to center it) and 16 is an offset that felt ok.
@ -45,7 +53,7 @@ export default class MessageComposerFormatBar extends React.PureComponent {
} }
hide() { hide() {
this._formatBarRef.classList.remove("mx_MessageComposerFormatBar_shown"); this.setState({visible: false});
} }
} }
@ -55,6 +63,7 @@ class FormatButton extends React.PureComponent {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
shortcut: PropTypes.string, shortcut: PropTypes.string,
visible: PropTypes.bool,
} }
render() { render() {
@ -72,7 +81,7 @@ class FormatButton extends React.PureComponent {
); );
return ( return (
<InteractiveTooltip content={tooltipContent}> <InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}>
<span aria-label={this.props.label} <span aria-label={this.props.label}
role="button" role="button"
onClick={this.props.onClick} onClick={this.props.onClick}

View File

@ -382,14 +382,15 @@ module.exports = createReactClass({
/>; />;
} }
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (notifBadges && mentionBadges && !isInvite) { if (notifBadges && mentionBadges && !isInvite) {
ariaLabel += " " + _t("It has %(count)s unread messages including mentions.", { ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: notificationCount, count: notificationCount,
}); });
} else if (notifBadges) { } else if (notifBadges) {
ariaLabel += " " + _t("It has %(count)s unread messages.", { count: notificationCount }); ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount });
} else if (mentionBadges && !isInvite) { } else if (mentionBadges && !isInvite) {
ariaLabel += " " + _t("It has unread mentions."); ariaLabel += " " + _t("Unread mentions.");
} }
return <AccessibleButton tabIndex="0" return <AccessibleButton tabIndex="0"

View File

@ -409,9 +409,9 @@ export default class Stickerpicker extends React.Component {
> >
</AccessibleButton>; </AccessibleButton>;
} }
return <div> return <React.Fragment>
{stickersButton} {stickersButton}
{this.state.showStickers && stickerPicker} {this.state.showStickers && stickerPicker}
</div>; </React.Fragment>;
} }
} }

View File

@ -121,7 +121,7 @@ export default class ThirdPartyMemberInfo extends React.Component {
// We shamelessly rip off the MemberInfo styles here. // We shamelessly rip off the MemberInfo styles here.
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo" role="tabpanel">
<div className="mx_MemberInfo_name"> <div className="mx_MemberInfo_name">
<AccessibleButton className="mx_MemberInfo_cancel" <AccessibleButton className="mx_MemberInfo_cancel"
onClick={this.onCancel} onClick={this.onCancel}

View File

@ -71,6 +71,9 @@ export default class HelpUserSettingsTab extends React.Component {
_onClearCacheAndReload = (e) => { _onClearCacheAndReload = (e) => {
if (!PlatformPeg.get()) return; if (!PlatformPeg.get()) return;
// Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly
// stopping in the middle of the logs.
console.log("Clear cache & reload clicked");
MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().store.deleteAllData().done(() => { MatrixClientPeg.get().store.deleteAllData().done(() => {
PlatformPeg.get().reload(); PlatformPeg.get().reload();
@ -226,7 +229,7 @@ export default class HelpUserSettingsTab extends React.Component {
</div> </div>
<div className='mx_HelpUserSettingsTab_debugButton'> <div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear Cache and Reload")} {_t("Clear cache and reload")}
</AccessibleButton> </AccessibleButton>
</div> </div>
</div> </div>

View File

@ -43,6 +43,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
'showJoinLeaves', 'showJoinLeaves',
'showAvatarChanges', 'showAvatarChanges',
'showDisplaynameChanges', 'showDisplaynameChanges',
'showImages',
]; ];
static ROOM_LIST_SETTINGS = [ static ROOM_LIST_SETTINGS = [

View File

@ -369,6 +369,7 @@
"Low bandwidth mode": "Low bandwidth mode", "Low bandwidth mode": "Low bandwidth mode",
"Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)",
"Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)",
"Show previews/thumbnails for images": "Show previews/thumbnails for images",
"Collecting app version information": "Collecting app version information", "Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs", "Collecting logs": "Collecting logs",
"Uploading report": "Uploading report", "Uploading report": "Uploading report",
@ -617,7 +618,7 @@
"Bug reporting": "Bug reporting", "Bug reporting": "Bug reporting",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.", "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.",
"Submit debug logs": "Submit debug logs", "Submit debug logs": "Submit debug logs",
"Clear Cache and Reload": "Clear Cache and Reload", "Clear cache and reload": "Clear cache and reload",
"FAQ": "FAQ", "FAQ": "FAQ",
"Versions": "Versions", "Versions": "Versions",
"matrix-react-sdk version:": "matrix-react-sdk version:", "matrix-react-sdk version:": "matrix-react-sdk version:",
@ -949,9 +950,9 @@
"Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>", "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Securely back up your keys to avoid losing them. <a>Learn more.</a>",
"Not now": "Not now", "Not now": "Not now",
"Don't ask me again": "Don't ask me again", "Don't ask me again": "Don't ask me again",
"It has %(count)s unread messages including mentions.|other": "It has %(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"It has %(count)s unread messages.|other": "It has %(count)s unread messages.", "%(count)s unread messages.|other": "%(count)s unread messages.",
"It has unread mentions.": "It has unread mentions.", "Unread mentions.": "Unread mentions.",
"Add a topic": "Add a topic", "Add a topic": "Add a topic",
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",
"This room has already been upgraded.": "This room has already been upgraded.", "This room has already been upgraded.": "This room has already been upgraded.",
@ -1036,6 +1037,7 @@
"Download %(text)s": "Download %(text)s", "Download %(text)s": "Download %(text)s",
"Invalid file%(extra)s": "Invalid file%(extra)s", "Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image", "Error decrypting image": "Error decrypting image",
"Show image": "Show image",
"Error decrypting video": "Error decrypting video", "Error decrypting video": "Error decrypting video",
"Agree": "Agree", "Agree": "Agree",
"Disagree": "Disagree", "Disagree": "Disagree",
@ -1124,6 +1126,7 @@
"No results": "No results", "No results": "No results",
"Yes": "Yes", "Yes": "Yes",
"No": "No", "No": "No",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"Communities": "Communities", "Communities": "Communities",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
"Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",

View File

@ -413,4 +413,9 @@ export const SETTINGS = {
), ),
default: true, default: true,
}, },
"showImages": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Show previews/thumbnails for images"),
default: true,
},
}; };

View File

@ -233,7 +233,9 @@ export default class WidgetUtils {
}; };
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const userWidgets = WidgetUtils.getUserWidgets(); // Get the current widgets and clone them before we modify them, otherwise
// we'll modify the content of the old event.
const userWidgets = JSON.parse(JSON.stringify(WidgetUtils.getUserWidgets()));
// Delete existing widget with ID // Delete existing widget with ID
try { try {