mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Merge remote-tracking branch 'origin/develop' into release-v1.6.2
This commit is contained in:
commit
a79dce1623
37 changed files with 489 additions and 195 deletions
|
@ -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";
|
||||
|
|
34
res/css/views/elements/_ErrorBoundary.scss
Normal file
34
res/css/views/elements/_ErrorBoundary.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
6
res/img/feather-customised/eye.svg
Normal file
6
res/img/feather-customised/eye.svg
Normal 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 |
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
104
src/components/views/elements/ErrorBoundary.js
Normal file
104
src/components/views/elements/ErrorBoundary.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -222,7 +222,7 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MemberList">
|
||||
<div className="mx_MemberList" role="tabpanel">
|
||||
{ inviteButton }
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
{ joined }
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -43,6 +43,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
];
|
||||
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -413,4 +413,9 @@ export const SETTINGS = {
|
|||
),
|
||||
default: true,
|
||||
},
|
||||
"showImages": {
|
||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||
displayName: _td("Show previews/thumbnails for images"),
|
||||
default: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue