From 47114a18e3d6dc2090beeb43d03f865d6436a99a Mon Sep 17 00:00:00 2001 From: Debanshu Kundu Date: Thu, 2 Mar 2017 23:17:27 +0530 Subject: [PATCH] PLT-5380 Moved link preview image to top right corner of preview area (#5212) * PLT-5380 Moved link preview image to top right corner of preview area for smaller images, larger and wide images are still shown below the text. Also added logic to hide image area if image loading fails. * Updating link previews css --- .../components/post_attachment_opengraph.jsx | 223 ++++++++++++------ webapp/sass/layout/_post.scss | 2 +- webapp/sass/layout/_webhooks.scss | 69 +++++- webapp/sass/responsive/_mobile.scss | 45 ++++ webapp/sass/responsive/_tablet.scss | 13 + webapp/tests/utils_get_nearest_point.test.jsx | 8 +- webapp/utils/commons.jsx | 18 +- 7 files changed, 282 insertions(+), 96 deletions(-) diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx index b83150839a..12437e672e 100644 --- a/webapp/components/post_view/components/post_attachment_opengraph.jsx +++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx @@ -11,29 +11,47 @@ import {requestOpenGraphMetadata} from 'actions/global_actions.jsx'; export default class PostAttachmentOpenGraph extends React.Component { constructor(props) { super(props); + this.largeImageMinWidth = 150; this.imageDimentions = { // Image dimentions in pixels. - height: 150, - width: 150 + height: 80, + width: 80 }; - this.maxDescriptionLength = 300; - this.descriptionEllipsis = '...'; + this.textMaxLenght = 300; + this.textEllipsis = '...'; + this.largeImageMinRatio = 16 / 9; + this.smallImageContainerLeftPadding = 15; + + this.imageRatio = null; + + this.smallImageContainer = null; + this.smallImageElement = null; + this.fetchData = this.fetchData.bind(this); this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this); this.toggleImageVisibility = this.toggleImageVisibility.bind(this); this.onImageLoad = this.onImageLoad.bind(this); + this.onImageError = this.onImageError.bind(this); + this.truncateText = this.truncateText.bind(this); + this.setImageWidth = this.setImageWidth.bind(this); + } + + IMAGE_LOADED = { + LOADING: 'loading', + YES: 'yes', + ERROR: 'error' } componentWillMount() { this.setState({ data: {}, - imageLoaded: false, - imageVisible: this.props.previewCollapsed.startsWith('false') + imageLoaded: this.IMAGE_LOADED.LOADING, + imageVisible: this.props.previewCollapsed.startsWith('false'), + hasLargeImage: false }); this.fetchData(this.props.link); } componentWillReceiveProps(nextProps) { - this.setState({imageVisible: nextProps.previewCollapsed.startsWith('false')}); if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) { this.fetchData(nextProps.link); } @@ -43,6 +61,9 @@ export default class PostAttachmentOpenGraph extends React.Component { if (nextState.imageVisible !== this.state.imageVisible) { return true; } + if (nextState.hasLargeImage !== this.state.hasLargeImage) { + return true; + } if (nextState.imageLoaded !== this.state.imageLoaded) { return true; } @@ -54,16 +75,20 @@ export default class PostAttachmentOpenGraph extends React.Component { componentDidMount() { OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange); + this.setImageWidth(); + window.addEventListener('resize', this.setImageWidth); } componentDidUpdate() { if (this.props.childComponentDidUpdateFunction) { this.props.childComponentDidUpdateFunction(); } + this.setImageWidth(); } componentWillUnmount() { OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange); + window.removeEventListener('resize', this.setImageWidth); } onOpenGraphMetadataChange(url) { @@ -74,53 +99,54 @@ export default class PostAttachmentOpenGraph extends React.Component { fetchData(url) { const data = OpenGraphStore.getOgInfo(url); - this.setState({data, imageLoaded: false}); + this.setState({data, imageLoaded: this.IMAGE_LOADED.LOADING}); if (Utils.isEmptyObject(data)) { requestOpenGraphMetadata(url); } } getBestImageUrl() { - if (this.state.data.images == null) { + if (Utils.isEmptyObject(this.state.data.images)) { return null; } - const nearestPointData = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height'); - - const bestImage = nearestPointData.nearestPoint; - const bestImageLte = nearestPointData.nearestPointLte; // Best image <= 150px height and width - - let finalBestImage; - - if ( - !Utils.isEmptyObject(bestImageLte) && - bestImageLte.height <= this.imageDimentions.height && - bestImageLte.width <= this.imageDimentions.width - ) { - finalBestImage = bestImageLte; - } else { - finalBestImage = bestImage; - } - - return finalBestImage.secure_url || finalBestImage.url; + const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height'); + return bestImage.secure_url || bestImage.url; } toggleImageVisibility() { this.setState({imageVisible: !this.state.imageVisible}); } - onImageLoad() { - this.setState({imageLoaded: true}); + onImageLoad(image) { + this.imageRatio = image.target.naturalWidth / image.target.naturalHeight; + if ( + image.target.naturalWidth >= this.largeImageMinWidth && + this.imageRatio >= this.largeImageMinRatio && + !this.state.hasLargeImage + ) { + this.setState({ + hasLargeImage: true + }); + } + this.setState({ + imageLoaded: this.IMAGE_LOADED.YES + }); + } + + onImageError() { + this.setState({imageLoaded: this.IMAGE_LOADED.ERROR}); } loadImage(src) { const img = new Image(); img.onload = this.onImageLoad; + img.onerror = this.onImageError; img.src = src; } imageToggleAnchoreTag(imageUrl) { - if (imageUrl) { + if (imageUrl && this.state.hasLargeImage) { return ( + wrapInSmallImageContainer(imageElement) { + return ( +
{ + this.smallImageContainer = div; + }} + > + {imageElement} +
+ ); + } + + imageTag(imageUrl, renderingForLargeImage = false) { + var element = null; + if ( + imageUrl && renderingForLargeImage === this.state.hasLargeImage && + (!renderingForLargeImage || (renderingForLargeImage && this.state.imageVisible)) + ) { + if (this.state.imageLoaded === this.IMAGE_LOADED.LOADING) { + if (renderingForLargeImage) { + element = ; + } else { + element = this.wrapInSmallImageContainer( + + ); + } + } else if (this.state.imageLoaded === this.IMAGE_LOADED.YES) { + if (renderingForLargeImage) { + element = ( + + ); + } else { + element = this.wrapInSmallImageContainer( + { + this.smallImageElement = img; + }} + /> + ); + } + } else if (this.state.imageLoaded === this.IMAGE_LOADED.ERROR) { + return null; + } + } + return element; + } + + setImageWidth() { + if ( + this.state.imageLoaded === this.IMAGE_LOADED.YES && + this.smallImageContainer && + this.smallImageElement + ) { + this.smallImageContainer.style.width = ( + (this.smallImageElement.offsetHeight * this.imageRatio) + + this.smallImageContainerLeftPadding + + 'px' ); } - return null; + } + + truncateText(text, maxLength = this.textMaxLenght, ellipsis = this.textEllipsis) { + if (text.length > maxLength) { + return text.substring(0, maxLength - ellipsis.length) + ellipsis; + } + return text; } render() { @@ -152,52 +243,52 @@ export default class PostAttachmentOpenGraph extends React.Component { const data = this.state.data; const imageUrl = this.getBestImageUrl(); - var description = data.description; - if (description.length > this.maxDescriptionLength) { - description = description.substring(0, this.maxDescriptionLength - this.descriptionEllipsis.length) + this.descriptionEllipsis; - } - - if (imageUrl && this.state.imageVisible) { + if (imageUrl) { this.loadImage(imageUrl); } return (
- {data.site_name} -

- {this.truncateText(data.site_name)} +

- {data.title || data.url || this.props.link} - -

-

+
+
- {description}   - {this.imageToggleAnchoreTag(imageUrl)} +
+ {this.truncateText(data.description)}   + {this.imageToggleAnchoreTag(imageUrl)} +
+ {this.imageTag(imageUrl, true)}
- {this.imageTag(imageUrl)}
+ {this.imageTag(imageUrl, false)}
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index bca10cae2c..ab391fa1d4 100644 --- a/webapp/sass/layout/_post.scss +++ b/webapp/sass/layout/_post.scss @@ -1191,7 +1191,7 @@ cursor: pointer; display: inline-block; font: normal normal normal 14px/1 FontAwesome; - margin: 0 0 10px; + margin: 0; text-rendering: auto; &.pull-left { diff --git a/webapp/sass/layout/_webhooks.scss b/webapp/sass/layout/_webhooks.scss index 904c50ccc5..f3a8c6fd39 100644 --- a/webapp/sass/layout/_webhooks.scss +++ b/webapp/sass/layout/_webhooks.scss @@ -38,6 +38,9 @@ .post { .attachment { + &.attachment--opengraph { + max-width: 800px; + } .attachment__content { border-radius: 4px; border-style: solid; @@ -68,11 +71,29 @@ &.attachment__container--danger { border-left-color: #e40303; } + &.attachment__container--opengraph { + display: table; + table-layout: fixed; + width: 100%; + margin: 0; + padding-bottom: 13px; + div { + margin: 0; + } + } .sitename { color: #A3A3A3; } } + .attachment__body__wrap { + &.attachment__body__wrap--opengraph { + display: table-cell; + width: 100%; + vertical-align: top; + } + } + .attachment__body { float: left; overflow-x: auto; @@ -83,13 +104,11 @@ &.attachment__body--no_thumb { width: 100%; } - .attachment__image { - margin-bottom: 0; - max-height: 150px; - max-width: 150px; - &.loading { - height: 150px; - } + &.attachment__body--opengraph { + float: none; + padding-right: 0; + width: 100%; + word-wrap: break-word; } } @@ -97,10 +116,38 @@ display: inline-block; } + .attachment__image__container--openraph { + display: table-cell; + vertical-align: top; + padding-top: 3px; + padding-left: 15px; + } + .attachment__image { margin-bottom: 1em; max-height: 300px; max-width: 500px; + + &.attachment__image--openraph { + margin-bottom: 0; + max-height: 80px; + max-width: 200px; + + &.loading { + height: 80px; + } + + &.large_image { + border-radius: 3px; + margin-top: 10px; + max-height: 200px; + max-width: 400px; + + &.loading { + height: 150px; + } + } + } } .attachment__author-name { @@ -121,6 +168,14 @@ overflow: hidden; white-space: nowrap; } + + &.attachment__title--opengraph { + height: auto; + word-wrap: break-word; + &.is-url { + word-break: break-all + } + } } .attachment-link-more { diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index d1fc10428f..3170fb0d4c 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -1210,6 +1210,15 @@ } } } + .post { + .attachment { + .attachment__image { + &.attachment__image--openraph { + max-width: 200px; + } + } + } + } } @media screen and (max-width: 640px) { @@ -1385,6 +1394,16 @@ text-align: left; } } + + .post { + .attachment { + .attachment__image { + &.attachment__image--openraph { + max-width: 200px; + } + } + } + } } @media screen and (max-width: 550px) { @@ -1415,6 +1434,15 @@ top: 60px; width: calc(100% - 30px); } + .post { + .attachment { + .attachment__image { + &.attachment__image--openraph { + max-width: 180px; + } + } + } + } } @media screen and (max-width: 480px) { @@ -1521,6 +1549,16 @@ .integration__icon { display: none; } + + .post { + .attachment { + .attachment__image { + &.attachment__image--openraph { + max-width: 120px; + } + } + } + } } @media screen and (max-height: 640px) { @@ -1553,6 +1591,13 @@ } } } + .attachment { + .attachment__image { + &.attachment__image--openraph { + max-width: 80px; + } + } + } } .tutorial-steps__container { diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss index 96a71694f7..06a725a312 100644 --- a/webapp/sass/responsive/_tablet.scss +++ b/webapp/sass/responsive/_tablet.scss @@ -128,6 +128,19 @@ } } } + .post { + .attachment { + .attachment__image { + &.attachment__image--openraph { + max-height: 70px; + max-width: 300px; + &.loading { + height: 70px; + } + } + } + } + } } // Tablet and desktop diff --git a/webapp/tests/utils_get_nearest_point.test.jsx b/webapp/tests/utils_get_nearest_point.test.jsx index b0b0a2e0e8..02ca29cc38 100644 --- a/webapp/tests/utils_get_nearest_point.test.jsx +++ b/webapp/tests/utils_get_nearest_point.test.jsx @@ -24,12 +24,10 @@ describe('CommonUtils.getNearestPoint', function() { nearestPointLte: {x: 1, y: 1} } ]) { - const nearestPointData = CommonUtils.getNearestPoint(data.pivotPoint, data.points); + const nearestPoint = CommonUtils.getNearestPoint(data.pivotPoint, data.points); - assert.equal(nearestPointData.nearestPoint.x, data.nearestPoint.x); - assert.equal(nearestPointData.nearestPoint.y, data.nearestPoint.y); - assert.equal(nearestPointData.nearestPointLte.x, data.nearestPointLte.x); - assert.equal(nearestPointData.nearestPointLte.y, data.nearestPointLte.y); + assert.equal(nearestPoint.x, data.nearestPoint.x); + assert.equal(nearestPoint.y, data.nearestPoint.y); } }); }); diff --git a/webapp/utils/commons.jsx b/webapp/utils/commons.jsx index 1888869dc8..224653df7a 100644 --- a/webapp/utils/commons.jsx +++ b/webapp/utils/commons.jsx @@ -8,7 +8,6 @@ export function getDistanceBW2Points(point1, point2, xAttr = 'x', yAttr = 'y') { */ export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') { var nearestPoint = {}; - var nearestPointLte = {}; // Nearest point smaller than or equal to point for (const point of points) { if (typeof nearestPoint[xAttr] === 'undefined' || typeof nearestPoint[yAttr] === 'undefined') { nearestPoint = point; @@ -16,21 +15,6 @@ export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') { // Check for bestImage nearestPoint = point; } - - if (typeof nearestPointLte[xAttr] === 'undefined' || typeof nearestPointLte[yAttr] === 'undefined') { - if (point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr]) { - nearestPointLte = point; - } - } else if ( - // Check for bestImageLte - getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPointLte, pivotPoint, xAttr, yAttr) && - point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr] - ) { - nearestPointLte = point; - } } - return { - nearestPoint, - nearestPointLte - }; + return nearestPoint; }