diff --git a/res/css/_components.scss b/res/css/_components.scss index 40a797dc15..f627fe3a29 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/elements/_ErrorBoundary.scss b/res/css/views/elements/_ErrorBoundary.scss new file mode 100644 index 0000000000..e46ba69a7c --- /dev/null +++ b/res/css/views/elements/_ErrorBoundary.scss @@ -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; + } +} diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 86bc022829..8e650eaff4 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -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; +} diff --git a/res/css/views/messages/_MStickerBody.scss b/res/css/views/messages/_MStickerBody.scss index e4977bcc34..162ee7da86 100644 --- a/res/css/views/messages/_MStickerBody.scss +++ b/res/css/views/messages/_MStickerBody.scss @@ -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; +} diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index b32a44219a..ce519b1ea7 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -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; diff --git a/res/img/feather-customised/eye.svg b/res/img/feather-customised/eye.svg new file mode 100644 index 0000000000..fd06bf7b21 --- /dev/null +++ b/res/img/feather-customised/eye.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Lifecycle.js b/src/Lifecycle.js index c03a958840..a2cfc9a1ba 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -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 diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 2b9594581e..fb2bdcad42 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -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 ( -
+
); diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index fd315d2540..36dd3a7a61 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -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) { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 5529fb8f32..66210e2f93 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -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); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2da219a28d..da67416400 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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 = (
); - } - - // 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 = ( ); - } - - 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 ( - ; } - return ( + view = (
{errorBox} - { _t('Logout') } + {_t('Logout')}
); } - } - - if (this.state.view === VIEWS.WELCOME) { + } else if (this.state.view === VIEWS.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); - return ; - } - - if (this.state.view === VIEWS.REGISTER) { + view = ; + } else if (this.state.view === VIEWS.REGISTER) { const Registration = sdk.getComponent('structures.auth.Registration'); - return ( + view = ( ); - } - - - if (this.state.view === VIEWS.FORGOT_PASSWORD) { + } else if (this.state.view === VIEWS.FORGOT_PASSWORD) { const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); - return ( + view = ( ); - } - - if (this.state.view === VIEWS.LOGIN) { + } else if (this.state.view === VIEWS.LOGIN) { const Login = sdk.getComponent('structures.auth.Login'); - return ( + view = ( ); - } - - if (this.state.view === VIEWS.SOFT_LOGOUT) { + } else if (this.state.view === VIEWS.SOFT_LOGOUT) { const SoftLogout = sdk.getComponent('structures.auth.SoftLogout'); - return ( + view = ( ); + } else { + console.error(`Unknown view ${this.state.view}`); } - console.error(`Unknown view ${this.state.view}`); + const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary'); + return + {view} + ; }, }); diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index f9ce0e008e..3a07bf2e63 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -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 ( -
+
); diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index f1057819ff..3d09c05c43 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -258,7 +258,7 @@ const RoomSubList = createReactClass({ const tabindex = this.props.isFiltered ? "0" : "-1"; return (
- + { chevron } {this.props.label} { incomingCall } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4d52158dae..27b9d93e50 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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 (
- + + +
); } else { @@ -1597,18 +1601,20 @@ module.exports = createReactClass({ const roomAlias = this.state.roomAlias; return (
- + + +
); } @@ -1618,12 +1624,14 @@ module.exports = createReactClass({ if (myMembership == 'invite') { if (this.state.joining || this.state.rejecting) { return ( - + + ); } else { const myUserId = MatrixClientPeg.get().credentials.userId; @@ -1638,14 +1646,16 @@ module.exports = createReactClass({ // We have a regular invite for this room. return (
- + + +
); } @@ -1942,41 +1952,43 @@ module.exports = createReactClass({ return (
- - -
- { auxPanel } -
- { topUnreadMessagesBar } - { jumpToBottom } - { messagePanel } - { searchResultsPanel } -
-
-
-
- { statusBar } + + + +
+ {auxPanel} +
+ {topUnreadMessagesBar} + {jumpToBottom} + {messagePanel} + {searchResultsPanel}
+
+
+
+ {statusBar} +
+
+ {previewBar} + {messageComposer}
- { previewBar } - { messageComposer } -
- + +
); }, diff --git a/src/components/views/elements/AccessibleTooltipButton.js b/src/components/views/elements/AccessibleTooltipButton.js index c9a08f6a47..c824ea4025 100644 --- a/src/components/views/elements/AccessibleTooltipButton.js +++ b/src/components/views/elements/AccessibleTooltipButton.js @@ -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} /> :
; return ( - + { tip } ); diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js new file mode 100644 index 0000000000..630b369caa --- /dev/null +++ b/src/components/views/elements/ErrorBoundary.js @@ -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
+
+

{_t("Something went wrong!")}

+

{_t( + "Please create a new issue " + + "on GitHub so that we can investigate this bug.", {}, { + newIssueLink: (sub) => { + return { sub }; + }, + }, + )}

+

{_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.", + )}

+ + {_t("Submit debug logs")} + + + {_t("Clear cache and reload")} + +
+
; + } + + return this.props.children; + } +} diff --git a/src/components/views/elements/InteractiveTooltip.js b/src/components/views/elements/InteractiveTooltip.js index 41d66ae629..0bb356c5ba 100644 --- a/src/components/views/elements/InteractiveTooltip.js +++ b/src/components/views/elements/InteractiveTooltip.js @@ -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; } diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 75e647aa4b..3dac90fc35 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -183,7 +183,7 @@ module.exports = createReactClass({ const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper'); return ( -
+
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index d13f54579d..433625419d 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -222,7 +222,7 @@ export default createReactClass({ } return ( -
+
{ inviteButton } { joined } diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index c6d07cee50..420c163769 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -214,7 +214,7 @@ module.exports = createReactClass({ const groupRoomName = this.state.groupRoom.displayname; return ( -
+
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index 81921568d0..d57d5e313f 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -153,7 +153,7 @@ export default createReactClass({ const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const TruncatedList = sdk.getComponent("elements.TruncatedList"); return ( -
+
{ inviteButton } , - ); + let imageElement; + if (!this.state.showImage) { + imageElement = ; + } else { + imageElement = ( + {content.body} + ); + } + 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 = {content.body}; + img = ( + {content.body} + ); + } + + if (!this.state.showImage) { + img = ; + 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 { ; } } + +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 ( +
+
+ + {_t("Show image")} +
+
+ ); + } +} diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index 6a4128dfa7..ed82d49576 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -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 ``, but it does need extra padding // which is added by mx_MStickerBody_wrapper wrapImage(contentUrl, children) { - return
{ children }
; + let onClick = null; + if (!this.state.showImage) { + onClick = this.onClick; + } + return
{ children }
; } // Placeholder to show in place of the sticker image if diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.js index 2c1e15898e..06b434c1ce 100644 --- a/src/components/views/right_panel/HeaderButton.js +++ b/src/components/views/right_panel/HeaderButton.js @@ -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 diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js index 2fa9935ab8..a01b511dc8 100644 --- a/src/components/views/right_panel/HeaderButtons.js +++ b/src/components/views/right_panel/HeaderButtons.js @@ -91,7 +91,7 @@ export default class HeaderButtons extends React.Component { render() { // inline style as this will be swapped around in future commits - return
+ return
{ this.renderButtons() }
; } diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index cfa096a763..d93fe76b46 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -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); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1127c53854..2c667b83df 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -1124,35 +1124,35 @@ module.exports = createReactClass({ } return ( -
-
- { backButton } - { e2eIconElement } -

{ memberName }

+
+
+ { backButton } + { e2eIconElement } +

{ memberName }

+
+ { avatarElement } +
+ +
+
+ { this.props.member.userId } +
+ { roomMemberDetails }
- { avatarElement } +
+
+ { this._renderUserOptions() } -
-
- { this.props.member.userId } -
- { roomMemberDetails } -
+ { adminTools } + + { startChat } + + { this._renderDevices() } + + { spinner }
- -
- { this._renderUserOptions() } - - { adminTools } - - { startChat } - - { this._renderDevices() } - - { spinner } -
-
+
); }, diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 1ecb04d442..0805c0342c 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -475,7 +475,7 @@ module.exports = createReactClass({ } return ( -
+
{ inviteButton }
diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js index 8090fb2ad5..95c896c6fc 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.js @@ -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 (
this._formatBarRef = ref}> - this.props.onAction("bold")} icon="Bold" /> - this.props.onAction("italics")} icon="Italic" /> - this.props.onAction("strikethrough")} icon="Strikethrough" /> - this.props.onAction("code")} icon="Code" /> - this.props.onAction("quote")} icon="Quote" /> + const classes = classNames("mx_MessageComposerFormatBar", { + "mx_MessageComposerFormatBar_shown": this.state.visible, + }); + return (
this._formatBarRef = ref}> + this.props.onAction("bold")} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} /> + this.props.onAction("italics")} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} /> + this.props.onAction("strikethrough")} icon="Strikethrough" visible={this.state.visible} /> + this.props.onAction("code")} icon="Code" visible={this.state.visible} /> + this.props.onAction("quote")} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
); } 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 ( - + ; } + // 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 ; } - return
+ return {stickersButton} {this.state.showStickers && stickerPicker} -
; + ; } } diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js index 754e32871f..db6ab479a3 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.js +++ b/src/components/views/rooms/ThirdPartyMemberInfo.js @@ -121,7 +121,7 @@ export default class ThirdPartyMemberInfo extends React.Component { // We shamelessly rip off the MemberInfo styles here. 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().store.deleteAllData().done(() => { PlatformPeg.get().reload(); @@ -226,7 +229,7 @@ export default class HelpUserSettingsTab extends React.Component {
- {_t("Clear Cache and Reload")} + {_t("Clear cache and reload")}
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index 6528c86f19..30f46754b7 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -43,6 +43,7 @@ export default class PreferencesUserSettingsTab extends React.Component { 'showJoinLeaves', 'showAvatarChanges', 'showDisplaynameChanges', + 'showImages', ]; static ROOM_LIST_SETTINGS = [ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5a683ce92d..d1eb4f99d5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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. Learn more.": "Securely back up your keys to avoid losing them.
Learn more.", "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 create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue 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", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index c82b7a0c71..102efaa75a 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -413,4 +413,9 @@ export const SETTINGS = { ), default: true, }, + "showImages": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Show previews/thumbnails for images"), + default: true, + }, }; diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 237db82365..36907da5ab 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -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 {