From 59b29e4a7f88d5ceca141e9b179310404c5fd54f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 27 Sep 2019 21:08:31 -0600 Subject: [PATCH 1/6] Add an option to hide image previews Applies to images, stickers, and URL previews. Fixes https://github.com/vector-im/riot-web/issues/10735 --- res/css/views/messages/_MImageBody.scss | 15 +++++ res/css/views/messages/_MStickerBody.scss | 11 ++++ src/components/views/messages/MImageBody.js | 62 ++++++++++++++----- src/components/views/messages/MStickerBody.js | 17 ++++- .../views/rooms/LinkPreviewWidget.js | 4 ++ .../settings/tabs/user/LabsUserSettingsTab.js | 1 + src/i18n/strings/en_EN.json | 3 + src/settings/Settings.js | 5 ++ 8 files changed, 102 insertions(+), 16 deletions(-) diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 86bc022829..e5c29ba9e0 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -19,6 +19,21 @@ limitations under the License. margin-right: 34px; } +.mx_MImageBody_hidden { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + text-align: center; + border: 1px dashed $input-border-color; + + // To center the text in the middle of the frame + display: flex; + align-items: center; + justify-content: center; +} + .mx_MImageBody_thumbnail { position: absolute; width: 100%; 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/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index de19d0026f..4f165ea7bd 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -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,21 @@ 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, + let imageElement = ( {content.body}, + alt={content.body} + onError={this.onImageError} + onLoad={this.onImageLoad} + /> ); + if (!this.state.showImage) { + imageElement = ( +
+ {_t("Click to show image")} +
+ ); + } + return this.wrapImage(contentUrl, imageElement); } infoWidth = this.state.loadedImageDimensions.naturalWidth; infoHeight = this.state.loadedImageDimensions.naturalHeight; @@ -362,13 +387,22 @@ export default class MImageBody extends React.Component { // 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 = ( +
+ {_t("Click to show image")} +
+ ); + } } if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index 6a4128dfa7..c3e734a385 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -19,10 +19,15 @@ limitations under the License. import React from 'react'; import MImageBody from './MImageBody'; import sdk from '../../../index'; +import {_t} from "../../../languageHandler"; 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 @@ -34,6 +39,14 @@ export default class MStickerBody extends MImageBody { // Placeholder to show in place of the sticker image if // img onLoad hasn't fired yet. getPlaceholder() { + if (!this.state.showImage) { + return ( + + {_t("Click to show sticker")} + + ); + } + const TintableSVG = sdk.getComponent('elements.TintableSvg'); return ; } 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/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 07a2bf722a..55bd821f95 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -55,6 +55,7 @@ export default class LabsUserSettingsTab extends React.Component { + ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c23cd6d324..2f496eef00 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", @@ -1032,7 +1033,9 @@ "Decrypt %(text)s": "Decrypt %(text)s", "Download %(text)s": "Download %(text)s", "Invalid file%(extra)s": "Invalid file%(extra)s", + "Click to show image": "Click to show image", "Error decrypting image": "Error decrypting image", + "Click to show sticker": "Click to show sticker", "Error decrypting video": "Error decrypting video", "Agree": "Agree", "Disagree": "Disagree", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index e0ff16c538..efbbcf214f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -409,4 +409,9 @@ export const SETTINGS = { ), default: true, }, + "showImages": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Show previews/thumbnails for images"), + default: true, + }, }; From daef5f757422032804663a92b7737b8dcca4577a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Oct 2019 09:32:42 -0600 Subject: [PATCH 2/6] Move setting to real settings --- src/components/views/settings/tabs/user/LabsUserSettingsTab.js | 1 - .../views/settings/tabs/user/PreferencesUserSettingsTab.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index 55bd821f95..07a2bf722a 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -55,7 +55,6 @@ export default class LabsUserSettingsTab extends React.Component { - ); 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 = [ From 4b0596b6b776d2b99feae086c496c99ca9912777 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Oct 2019 17:00:01 -0600 Subject: [PATCH 3/6] Apply lipstick to hidden image design --- res/css/views/messages/_MImageBody.scss | 48 +++++++++++++------ res/img/feather-customised/eye.svg | 6 +++ src/components/views/messages/MImageBody.js | 39 ++++++++++----- src/components/views/messages/MStickerBody.js | 14 ++---- src/i18n/strings/en_EN.json | 2 +- 5 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 res/img/feather-customised/eye.svg diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index e5c29ba9e0..8e650eaff4 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -19,21 +19,6 @@ limitations under the License. margin-right: 34px; } -.mx_MImageBody_hidden { - position: absolute; - left: 0; - top: 0; - bottom: 0; - right: 0; - text-align: center; - border: 1px dashed $input-border-color; - - // To center the text in the middle of the frame - display: flex; - align-items: center; - justify-content: center; -} - .mx_MImageBody_thumbnail { position: absolute; width: 100%; @@ -74,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/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/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 4f165ea7bd..848cad6fb6 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -346,11 +346,7 @@ export default class MImageBody extends React.Component { /> ); if (!this.state.showImage) { - imageElement = ( -
- {_t("Click to show image")} -
- ); + imageElement = ; } return this.wrapImage(contentUrl, imageElement); } @@ -381,7 +377,7 @@ 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 @@ -396,13 +392,11 @@ export default class MImageBody extends React.Component { onMouseEnter={this.onImageEnter} onMouseLeave={this.onImageLeave} /> ); - if (!this.state.showImage) { - img = ( -
- {_t("Click to show image")} -
- ); - } + } + + 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) { @@ -488,3 +482,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 c3e734a385..1bd1c1f2eb 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -33,20 +33,16 @@ export default class MStickerBody extends MImageBody { // 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 // img onLoad hasn't fired yet. getPlaceholder() { - if (!this.state.showImage) { - return ( -
- {_t("Click to show sticker")} - - ); - } - const TintableSVG = sdk.getComponent('elements.TintableSvg'); return ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4844d409bc..9fa2546bad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1036,8 +1036,8 @@ "Decrypt %(text)s": "Decrypt %(text)s", "Download %(text)s": "Download %(text)s", "Invalid file%(extra)s": "Invalid file%(extra)s", - "Click to show image": "Click to show image", "Error decrypting image": "Error decrypting image", + "Show image": "Show image", "Click to show sticker": "Click to show sticker", "Error decrypting video": "Error decrypting video", "Agree": "Agree", From cf4fa068c445be2e8e309f794897d4ea2740c374 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Oct 2019 17:06:21 -0600 Subject: [PATCH 4/6] Remove old string --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9fa2546bad..43d6bd570d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1038,7 +1038,6 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", - "Click to show sticker": "Click to show sticker", "Error decrypting video": "Error decrypting video", "Agree": "Agree", "Disagree": "Disagree", From a719623bb99ef7d4fd0cbe3b16e63827c7f43d60 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 1 Oct 2019 17:06:55 -0600 Subject: [PATCH 5/6] Appease the linter --- src/components/views/messages/MStickerBody.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js index 1bd1c1f2eb..ed82d49576 100644 --- a/src/components/views/messages/MStickerBody.js +++ b/src/components/views/messages/MStickerBody.js @@ -14,12 +14,9 @@ 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'; -import {_t} from "../../../languageHandler"; export default class MStickerBody extends MImageBody { // Mostly empty to prevent default behaviour of MImageBody From 37d16db0f083005c8beb5c7d5b6dfd3e0bd25361 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Oct 2019 12:46:21 -0600 Subject: [PATCH 6/6] Elsify --- src/components/views/messages/MImageBody.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 848cad6fb6..e89c6d2bc9 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -338,15 +338,17 @@ 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) { - let imageElement = ( - {content.body} - ); + let imageElement; if (!this.state.showImage) { imageElement = ; + } else { + imageElement = ( + {content.body} + ); } return this.wrapImage(contentUrl, imageElement); }