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

This commit is contained in:
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/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_Field.scss";
@import "./views/elements/_ImageView.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;
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;
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;
word-wrap: break-word;
outline: none;
overflow-x: auto;
overflow-x: hidden;
span.mx_UserPill, span.mx_RoomPill {
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).
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
stopMatrixClient(/*unsetClient=*/false);
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"
resizeNotifier={this.props.resizeNotifier}
empty={_t('There are no visible files in this room')}
role="tabpanel"
/>
);
} else {
return (
<div className="mx_FilePanel">
<div className="mx_FilePanel" role="tabpanel">
<Loader />
</div>
);

View file

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

View file

@ -401,6 +401,12 @@ const LoggedInView = createReactClass({
const isClickShortcut = ev.target !== document.body &&
(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)) {
// synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true);

View file

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

View file

@ -258,7 +258,7 @@ const RoomSubList = createReactClass({
const tabindex = this.props.isFiltered ? "0" : "-1";
return (
<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 }
<span>{this.props.label}</span>
{ incomingCall }

View file

@ -1417,7 +1417,8 @@ module.exports = createReactClass({
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
// occasionally happen when no scroll state has been set on the
// messagePanel (ie, we didn't have an initial event (so it's
@ -1566,20 +1567,23 @@ module.exports = createReactClass({
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
if (!this.state.room) {
const loading = this.state.roomLoading || this.state.peekLoading;
if (loading) {
return (
<div className="mx_RoomView">
<RoomPreviewBar
canPreview={false}
previewLoading={this.state.peekLoading}
error={this.state.roomLoadError}
loading={loading}
joining={this.state.joining}
oobData={this.props.oobData}
/>
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
previewLoading={this.state.peekLoading}
error={this.state.roomLoadError}
loading={loading}
joining={this.state.joining}
oobData={this.props.oobData}
/>
</ErrorBoundary>
</div>
);
} else {
@ -1597,18 +1601,20 @@ module.exports = createReactClass({
const roomAlias = this.state.roomAlias;
return (
<div className="mx_RoomView">
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias}
joining={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
oobData={this.props.oobData}
signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
room={this.state.room}
/>
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias}
joining={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
oobData={this.props.oobData}
signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
room={this.state.room}
/>
</ErrorBoundary>
</div>
);
}
@ -1618,12 +1624,14 @@ module.exports = createReactClass({
if (myMembership == 'invite') {
if (this.state.joining || this.state.rejecting) {
return (
<RoomPreviewBar
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
error={this.state.roomLoadError}
joining={this.state.joining}
rejecting={this.state.rejecting}
/>
</ErrorBoundary>
);
} else {
const myUserId = MatrixClientPeg.get().credentials.userId;
@ -1638,14 +1646,16 @@ module.exports = createReactClass({
// We have a regular invite for this room.
return (
<div className="mx_RoomView">
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked}
inviterName={inviterName}
canPreview={false}
joining={this.state.joining}
room={this.state.room}
/>
<ErrorBoundary>
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked}
inviterName={inviterName}
canPreview={false}
joining={this.state.joining}
room={this.state.room}
/>
</ErrorBoundary>
</div>
);
}
@ -1942,41 +1952,43 @@ module.exports = createReactClass({
return (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
collapsedRhs={collapsedRhs}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
collapsedRhs={collapsedRhs}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>
{ auxPanel }
<div className="mx_RoomView_timeline">
{ topUnreadMessagesBar }
{ jumpToBottom }
{ messagePanel }
{ searchResultsPanel }
</div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar }
<ErrorBoundary>
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData}
inRoom={myMembership === 'join'}
collapsedRhs={collapsedRhs}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
collapsedRhs={collapsedRhs}
resizeNotifier={this.props.resizeNotifier}
>
<div className={fadableSectionClasses}>
{auxPanel}
<div className="mx_RoomView_timeline">
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}
{searchResultsPanel}
</div>
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar}
</div>
</div>
{previewBar}
{messageComposer}
</div>
{ previewBar }
{ messageComposer }
</div>
</MainSplit>
</MainSplit>
</ErrorBoundary>
</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");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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
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.
*/
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 PropTypes from 'prop-types';
@ -55,7 +56,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
label={title}
/> : <div />;
return (
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<AccessibleButton {...props} onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} aria-label={title}>
{ tip }
</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,
// Function to call when visibility of the tooltip changes
onVisibilityChange: PropTypes.func,
// flag to forcefully hide this tooltip
forceHidden: PropTypes.bool,
};
constructor() {
@ -269,8 +271,8 @@ export default class InteractiveTooltip extends React.Component {
renderTooltip() {
const { contentRect, visible } = this.state;
if (!visible) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
if (this.props.forceHidden === true || !visible) {
ReactDOM.render(null, getOrCreateContainer());
return null;
}

View file

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

View file

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

View file

@ -214,7 +214,7 @@ module.exports = createReactClass({
const groupRoomName = this.state.groupRoom.displayname;
return (
<div className="mx_MemberInfo">
<div className="mx_MemberInfo" role="tabpanel">
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
<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 TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_GroupRoomList">
<div className="mx_GroupRoomList" role="tabpanel">
{ inviteButton }
<GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}

View file

@ -64,6 +64,7 @@ export default class MImageBody extends React.Component {
imgLoaded: false,
loadedImageDimensions: null,
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) {
if (ev.button === 0 && !ev.metaKey) {
ev.preventDefault();
if (!this.state.showImage) {
this.showImage();
return;
}
const content = this.props.mxEvent.getContent();
const httpUrl = this._getContentUrl();
const ImageView = sdk.getComponent("elements.ImageView");
@ -120,7 +131,7 @@ export default class MImageBody extends React.Component {
onImageEnter(e) {
this.setState({ hover: true });
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
const imgElement = e.target;
@ -130,7 +141,7 @@ export default class MImageBody extends React.Component {
onImageLeave(e) {
this.setState({ hover: false });
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
if (!this.state.showImage || !this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return;
}
const imgElement = e.target;
@ -280,6 +291,12 @@ export default class MImageBody extends React.Component {
});
}).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();
}
@ -321,13 +338,19 @@ export default class MImageBody extends React.Component {
// By doing this, the image "pops" into the timeline, but is still restricted
// by the same width and height logic below.
if (!this.state.loadedImageDimensions) {
return this.wrapImage(contentUrl,
<img style={{display: 'none'}} src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
/>,
);
let imageElement;
if (!this.state.showImage) {
imageElement = <HiddenImagePlaceholder />;
} else {
imageElement = (
<img style={{display: 'none'}} src={thumbUrl} ref="image"
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
/>
);
}
return this.wrapImage(contentUrl, imageElement);
}
infoWidth = this.state.loadedImageDimensions.naturalWidth;
infoHeight = this.state.loadedImageDimensions.naturalHeight;
@ -356,19 +379,26 @@ export default class MImageBody extends React.Component {
placeholder = this.getPlaceholder();
}
const showPlaceholder = Boolean(placeholder);
let showPlaceholder = Boolean(placeholder);
if (thumbUrl && !this.state.imgError) {
// Restrict the width of the thumbnail here, otherwise it will fill the container
// which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size
img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
style={{ maxWidth: maxWidth + "px" }}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />;
img = (
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
style={{ maxWidth: maxWidth + "px" }}
alt={content.body}
onError={this.onImageError}
onLoad={this.onImageLoad}
onMouseEnter={this.onImageEnter}
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) {
@ -454,3 +484,22 @@ export default class MImageBody extends React.Component {
</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.
*/
'use strict';
import React from 'react';
import MImageBody from './MImageBody';
import sdk from '../../../index';
export default class MStickerBody extends MImageBody {
// Empty to prevent default behaviour of MImageBody
onClick() {
// Mostly empty to prevent default behaviour of MImageBody
onClick(ev) {
ev.preventDefault();
if (!this.state.showImage) {
this.showImage();
}
}
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
// which is added by mx_MStickerBody_wrapper
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

View file

@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 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");
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
aria-label={this.props.title}
aria-expanded={this.props.isHighlighted}
aria-selected={this.props.isHighlighted}
role="tab"
title={this.props.title}
className={classes}
onClick={this.onClick}>

View file

@ -91,7 +91,7 @@ export default class HeaderButtons extends React.Component {
render() {
// 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() }
</div>;
}

View file

@ -18,6 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { linkifyElement } from '../../../HtmlUtils';
import SettingsStore from "../../../settings/SettingsStore";
const sdk = require('../../../index');
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?
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;
if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);

View file

@ -1124,35 +1124,35 @@ module.exports = createReactClass({
}
return (
<div className="mx_MemberInfo">
<div className="mx_MemberInfo_name">
{ backButton }
{ e2eIconElement }
<h2>{ memberName }</h2>
<div className="mx_MemberInfo" role="tabpanel">
<div className="mx_MemberInfo_name">
{ backButton }
{ e2eIconElement }
<h2>{ memberName }</h2>
</div>
{ avatarElement }
<div className="mx_MemberInfo_container">
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
{ roomMemberDetails }
</div>
{ avatarElement }
</div>
<AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
<div className="mx_MemberInfo_container">
{ this._renderUserOptions() }
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
{ roomMemberDetails }
</div>
{ adminTools }
{ startChat }
{ this._renderDevices() }
{ spinner }
</div>
<AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
<div className="mx_MemberInfo_container">
{ this._renderUserOptions() }
{ adminTools }
{ startChat }
{ this._renderDevices() }
{ spinner }
</div>
</AutoHideScrollbar>
</AutoHideScrollbar>
</div>
);
},

View file

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

View file

@ -18,7 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import classNames from 'classnames';
export default class MessageComposerFormatBar extends React.PureComponent {
static propTypes = {
@ -26,18 +26,26 @@ export default class MessageComposerFormatBar extends React.PureComponent {
shortcuts: PropTypes.object.isRequired,
}
constructor(props) {
super(props);
this.state = {visible: false};
}
render() {
return (<div className="mx_MessageComposerFormatBar" ref={ref => this._formatBarRef = ref}>
<FormatButton shortcut={this.props.shortcuts.bold} label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" />
<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" />
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction("code")} icon="Code" />
<FormatButton shortcut={this.props.shortcuts.quote} label={_t("Quote")} onClick={() => this.props.onAction("quote")} icon="Quote" />
const classes = classNames("mx_MessageComposerFormatBar", {
"mx_MessageComposerFormatBar_shown": this.state.visible,
});
return (<div className={classes} ref={ref => this._formatBarRef = ref}>
<FormatButton label={_t("Bold")} onClick={() => this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
<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>);
}
showAt(selectionRect) {
this._formatBarRef.classList.add("mx_MessageComposerFormatBar_shown");
this.setState({visible: true});
const parentRect = this._formatBarRef.parentElement.getBoundingClientRect();
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.
@ -45,7 +53,7 @@ export default class MessageComposerFormatBar extends React.PureComponent {
}
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,
icon: PropTypes.string.isRequired,
shortcut: PropTypes.string,
visible: PropTypes.bool,
}
render() {
@ -72,7 +81,7 @@ class FormatButton extends React.PureComponent {
);
return (
<InteractiveTooltip content={tooltipContent}>
<InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}>
<span aria-label={this.props.label}
role="button"
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) {
ariaLabel += " " + _t("It has %(count)s unread messages including mentions.", {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: notificationCount,
});
} 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) {
ariaLabel += " " + _t("It has unread mentions.");
ariaLabel += " " + _t("Unread mentions.");
}
return <AccessibleButton tabIndex="0"

View file

@ -409,9 +409,9 @@ export default class Stickerpicker extends React.Component {
>
</AccessibleButton>;
}
return <div>
return <React.Fragment>
{stickersButton}
{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.
return (
<div className="mx_MemberInfo">
<div className="mx_MemberInfo" role="tabpanel">
<div className="mx_MemberInfo_name">
<AccessibleButton className="mx_MemberInfo_cancel"
onClick={this.onCancel}

View file

@ -71,6 +71,9 @@ export default class HelpUserSettingsTab extends React.Component {
_onClearCacheAndReload = (e) => {
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().store.deleteAllData().done(() => {
PlatformPeg.get().reload();
@ -226,7 +229,7 @@ export default class HelpUserSettingsTab extends React.Component {
</div>
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear Cache and Reload")}
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
</div>

View file

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

View file

@ -369,6 +369,7 @@
"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)",
"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 logs": "Collecting logs",
"Uploading report": "Uploading report",
@ -617,7 +618,7 @@
"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.",
"Submit debug logs": "Submit debug logs",
"Clear Cache and Reload": "Clear Cache and Reload",
"Clear cache and reload": "Clear cache and reload",
"FAQ": "FAQ",
"Versions": "Versions",
"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>",
"Not now": "Not now",
"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.",
"It has %(count)s unread messages.|other": "It has %(count)s unread messages.",
"It has unread mentions.": "It has unread mentions.",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages.|other": "%(count)s unread messages.",
"Unread mentions.": "Unread mentions.",
"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.",
"This room has already been upgraded.": "This room has already been upgraded.",
@ -1036,6 +1037,7 @@
"Download %(text)s": "Download %(text)s",
"Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image",
"Show image": "Show image",
"Error decrypting video": "Error decrypting video",
"Agree": "Agree",
"Disagree": "Disagree",
@ -1124,6 +1126,7 @@
"No results": "No results",
"Yes": "Yes",
"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",
"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",

View file

@ -413,4 +413,9 @@ export const SETTINGS = {
),
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 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
try {