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
This commit is contained in:
Debanshu Kundu
2017-03-02 23:17:27 +05:30
committed by enahum
parent d9a297d629
commit 47114a18e3
7 changed files with 282 additions and 96 deletions

View File

@@ -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 (
<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}
<span className='sitename'>{this.truncateText(data.site_name)}</span>
<h1
className={'attachment__title attachment__title--opengraph' + (data.title ? '' : ' is-url')}
>
{data.title || data.url || this.props.link}
</a>
</h1>
<div >
<div
className={'attachment__body attachment__body--no_thumb'}
>
<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} &nbsp;
{this.imageToggleAnchoreTag(imageUrl)}
<div>
{this.truncateText(data.description)} &nbsp;
{this.imageToggleAnchoreTag(imageUrl)}
</div>
{this.imageTag(imageUrl, true)}
</div>
{this.imageTag(imageUrl)}
</div>
</div>
</div>
{this.imageTag(imageUrl, false)}
</div>
</div>
</div>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -128,6 +128,19 @@
}
}
}
.post {
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-height: 70px;
max-width: 300px;
&.loading {
height: 70px;
}
}
}
}
}
}
// Tablet and desktop

View File

@@ -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);
}
});
});

View File

@@ -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;
}