diff options
-rw-r--r-- | webapp/components/post_view/components/post_attachment_opengraph.jsx | 221 | ||||
-rw-r--r-- | webapp/sass/layout/_post.scss | 2 | ||||
-rw-r--r-- | webapp/sass/layout/_webhooks.scss | 69 | ||||
-rw-r--r-- | webapp/sass/responsive/_mobile.scss | 45 | ||||
-rw-r--r-- | webapp/sass/responsive/_tablet.scss | 13 | ||||
-rw-r--r-- | webapp/tests/utils_get_nearest_point.test.jsx | 8 | ||||
-rw-r--r-- | webapp/utils/commons.jsx | 18 |
7 files changed, 281 insertions, 95 deletions
diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx index b83150839..12437e672 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 + const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height'); + return bestImage.secure_url || bestImage.url; + } - let finalBestImage; + toggleImageVisibility() { + this.setState({imageVisible: !this.state.imageVisible}); + } + onImageLoad(image) { + this.imageRatio = image.target.naturalWidth / image.target.naturalHeight; if ( - !Utils.isEmptyObject(bestImageLte) && - bestImageLte.height <= this.imageDimentions.height && - bestImageLte.width <= this.imageDimentions.width + image.target.naturalWidth >= this.largeImageMinWidth && + this.imageRatio >= this.largeImageMinRatio && + !this.state.hasLargeImage ) { - finalBestImage = bestImageLte; - } else { - finalBestImage = bestImage; + this.setState({ + hasLargeImage: true + }); } - - return finalBestImage.secure_url || finalBestImage.url; - } - - toggleImageVisibility() { - this.setState({imageVisible: !this.state.imageVisible}); + this.setState({ + imageLoaded: this.IMAGE_LOADED.YES + }); } - onImageLoad() { - this.setState({imageLoaded: true}); + 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 ( <a className={'post__embed-visibility'} @@ -133,16 +159,81 @@ export default class PostAttachmentOpenGraph extends React.Component { return null; } - imageTag(imageUrl) { - if (imageUrl && this.state.imageVisible) { - return ( - <img - className={this.state.imageLoaded ? 'attachment__image' : 'attachment__image loading'} - src={this.state.imageLoaded ? imageUrl : null} - /> + wrapInSmallImageContainer(imageElement) { + return ( + <div + className='attachment__image__container--openraph' + style={{ + width: (this.imageDimentions.height * this.imageRatio) + this.smallImageContainerLeftPadding + }} // Initially set the width accordinly to max image heigh, ie 80px. Later on it would be modified according to actul height of image. + ref={(div) => { + this.smallImageContainer = div; + }} + > + {imageElement} + </div> + ); + } + + 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 = <img className={'attachment__image attachment__image--openraph loading large_image'}/>; + } else { + element = this.wrapInSmallImageContainer( + <img className={'attachment__image attachment__image--openraph loading '}/> + ); + } + } else if (this.state.imageLoaded === this.IMAGE_LOADED.YES) { + if (renderingForLargeImage) { + element = ( + <img + className={'attachment__image attachment__image--openraph large_image'} + src={imageUrl} + /> + ); + } else { + element = this.wrapInSmallImageContainer( + <img + className={'attachment__image attachment__image--openraph'} + src={imageUrl} + ref={(img) => { + 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 ( <div - className='attachment attachment--oembed' + className='attachment attachment--opengraph' ref='attachment' > <div className='attachment__content'> <div - className={'clearfix attachment__container'} + className={'clearfix attachment__container attachment__container--opengraph'} > - <span className='sitename'>{data.site_name}</span> - <h1 - className='attachment__title has-link' + <div + className={'attachment__body__wrap attachment__body__wrap--opengraph'} > - <a - className='attachment__title-link' - href={data.url || this.props.link} - target='_blank' - rel='noopener noreferrer' - title={data.title || data.url || this.props.link} - > - {data.title || data.url || this.props.link} - </a> - </h1> - <div > - <div - className={'attachment__body attachment__body--no_thumb'} + <span className='sitename'>{this.truncateText(data.site_name)}</span> + <h1 + className={'attachment__title attachment__title--opengraph' + (data.title ? '' : ' is-url')} > - <div> + <a + className='attachment__title-link attachment__title-link--opengraph' + href={data.url || this.props.link} + target='_blank' + rel='noopener noreferrer' + title={data.title || data.url || this.props.link} + > + {this.truncateText(data.title || data.url || this.props.link)} + </a> + </h1> + <div > + <div + className={'attachment__body attachment__body--opengraph'} + > <div> - {description} - {this.imageToggleAnchoreTag(imageUrl)} + <div> + {this.truncateText(data.description)} + {this.imageToggleAnchoreTag(imageUrl)} + </div> + {this.imageTag(imageUrl, true)} </div> - {this.imageTag(imageUrl)} </div> </div> </div> + {this.imageTag(imageUrl, false)} </div> </div> </div> diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index bca10cae2..ab391fa1d 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 904c50ccc..f3a8c6fd3 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 d1fc10428..3170fb0d4 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 96a71694f..06a725a31 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 b0b0a2e0e..02ca29cc3 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 1888869dc..224653df7 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; } |