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
Этот коммит содержится в:
Debanshu Kundu
2017-03-02 23:17:27 +05:30
коммит произвёл enahum
родитель d9a297d629
Коммит 47114a18e3
7 изменённых файлов: 282 добавлений и 96 удалений

Просмотреть файл

@@ -11,29 +11,47 @@ import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
export default class PostAttachmentOpenGraph extends React.Component { export default class PostAttachmentOpenGraph extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.largeImageMinWidth = 150;
this.imageDimentions = { // Image dimentions in pixels. this.imageDimentions = { // Image dimentions in pixels.
height: 150, height: 80,
width: 150 width: 80
}; };
this.maxDescriptionLength = 300; this.textMaxLenght = 300;
this.descriptionEllipsis = '...'; 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.fetchData = this.fetchData.bind(this);
this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this); this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
this.toggleImageVisibility = this.toggleImageVisibility.bind(this); this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
this.onImageLoad = this.onImageLoad.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() { componentWillMount() {
this.setState({ this.setState({
data: {}, data: {},
imageLoaded: false, imageLoaded: this.IMAGE_LOADED.LOADING,
imageVisible: this.props.previewCollapsed.startsWith('false') imageVisible: this.props.previewCollapsed.startsWith('false'),
hasLargeImage: false
}); });
this.fetchData(this.props.link); this.fetchData(this.props.link);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.setState({imageVisible: nextProps.previewCollapsed.startsWith('false')});
if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) { if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) {
this.fetchData(nextProps.link); this.fetchData(nextProps.link);
} }
@@ -43,6 +61,9 @@ export default class PostAttachmentOpenGraph extends React.Component {
if (nextState.imageVisible !== this.state.imageVisible) { if (nextState.imageVisible !== this.state.imageVisible) {
return true; return true;
} }
if (nextState.hasLargeImage !== this.state.hasLargeImage) {
return true;
}
if (nextState.imageLoaded !== this.state.imageLoaded) { if (nextState.imageLoaded !== this.state.imageLoaded) {
return true; return true;
} }
@@ -54,16 +75,20 @@ export default class PostAttachmentOpenGraph extends React.Component {
componentDidMount() { componentDidMount() {
OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange); OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
this.setImageWidth();
window.addEventListener('resize', this.setImageWidth);
} }
componentDidUpdate() { componentDidUpdate() {
if (this.props.childComponentDidUpdateFunction) { if (this.props.childComponentDidUpdateFunction) {
this.props.childComponentDidUpdateFunction(); this.props.childComponentDidUpdateFunction();
} }
this.setImageWidth();
} }
componentWillUnmount() { componentWillUnmount() {
OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange); OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
window.removeEventListener('resize', this.setImageWidth);
} }
onOpenGraphMetadataChange(url) { onOpenGraphMetadataChange(url) {
@@ -74,53 +99,54 @@ export default class PostAttachmentOpenGraph extends React.Component {
fetchData(url) { fetchData(url) {
const data = OpenGraphStore.getOgInfo(url); const data = OpenGraphStore.getOgInfo(url);
this.setState({data, imageLoaded: false}); this.setState({data, imageLoaded: this.IMAGE_LOADED.LOADING});
if (Utils.isEmptyObject(data)) { if (Utils.isEmptyObject(data)) {
requestOpenGraphMetadata(url); requestOpenGraphMetadata(url);
} }
} }
getBestImageUrl() { getBestImageUrl() {
if (this.state.data.images == null) { if (Utils.isEmptyObject(this.state.data.images)) {
return null; return null;
} }
const nearestPointData = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height'); const bestImage = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height');
return bestImage.secure_url || bestImage.url;
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;
} }
toggleImageVisibility() { toggleImageVisibility() {
this.setState({imageVisible: !this.state.imageVisible}); this.setState({imageVisible: !this.state.imageVisible});
} }
onImageLoad() { onImageLoad(image) {
this.setState({imageLoaded: true}); 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) { loadImage(src) {
const img = new Image(); const img = new Image();
img.onload = this.onImageLoad; img.onload = this.onImageLoad;
img.onerror = this.onImageError;
img.src = src; img.src = src;
} }
imageToggleAnchoreTag(imageUrl) { imageToggleAnchoreTag(imageUrl) {
if (imageUrl) { if (imageUrl && this.state.hasLargeImage) {
return ( return (
<a <a
className={'post__embed-visibility'} className={'post__embed-visibility'}
@@ -133,17 +159,82 @@ export default class PostAttachmentOpenGraph extends React.Component {
return null; return null;
} }
imageTag(imageUrl) { wrapInSmallImageContainer(imageElement) {
if (imageUrl && this.state.imageVisible) {
return ( 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 <img
className={this.state.imageLoaded ? 'attachment__image' : 'attachment__image loading'} className={'attachment__image attachment__image--openraph large_image'}
src={this.state.imageLoaded ? imageUrl : null} 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 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'
);
}
}
truncateText(text, maxLength = this.textMaxLenght, ellipsis = this.textEllipsis) {
if (text.length > maxLength) {
return text.substring(0, maxLength - ellipsis.length) + ellipsis;
}
return text;
}
render() { render() {
if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) { if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
@@ -152,53 +243,53 @@ export default class PostAttachmentOpenGraph extends React.Component {
const data = this.state.data; const data = this.state.data;
const imageUrl = this.getBestImageUrl(); const imageUrl = this.getBestImageUrl();
var description = data.description;
if (description.length > this.maxDescriptionLength) { if (imageUrl) {
description = description.substring(0, this.maxDescriptionLength - this.descriptionEllipsis.length) + this.descriptionEllipsis;
}
if (imageUrl && this.state.imageVisible) {
this.loadImage(imageUrl); this.loadImage(imageUrl);
} }
return ( return (
<div <div
className='attachment attachment--oembed' className='attachment attachment--opengraph'
ref='attachment' ref='attachment'
> >
<div className='attachment__content'> <div className='attachment__content'>
<div <div
className={'clearfix attachment__container'} className={'clearfix attachment__container attachment__container--opengraph'}
> >
<span className='sitename'>{data.site_name}</span> <div
className={'attachment__body__wrap attachment__body__wrap--opengraph'}
>
<span className='sitename'>{this.truncateText(data.site_name)}</span>
<h1 <h1
className='attachment__title has-link' className={'attachment__title attachment__title--opengraph' + (data.title ? '' : ' is-url')}
> >
<a <a
className='attachment__title-link' className='attachment__title-link attachment__title-link--opengraph'
href={data.url || this.props.link} href={data.url || this.props.link}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'
title={data.title || data.url || this.props.link} title={data.title || data.url || this.props.link}
> >
{data.title || data.url || this.props.link} {this.truncateText(data.title || data.url || this.props.link)}
</a> </a>
</h1> </h1>
<div > <div >
<div <div
className={'attachment__body attachment__body--no_thumb'} className={'attachment__body attachment__body--opengraph'}
> >
<div> <div>
<div> <div>
{description} &nbsp; {this.truncateText(data.description)} &nbsp;
{this.imageToggleAnchoreTag(imageUrl)} {this.imageToggleAnchoreTag(imageUrl)}
</div> </div>
{this.imageTag(imageUrl)} {this.imageTag(imageUrl, true)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{this.imageTag(imageUrl, false)}
</div>
</div> </div>
</div> </div>
); );

Просмотреть файл

@@ -1191,7 +1191,7 @@
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
margin: 0 0 10px; margin: 0;
text-rendering: auto; text-rendering: auto;
&.pull-left { &.pull-left {

Просмотреть файл

@@ -38,6 +38,9 @@
.post { .post {
.attachment { .attachment {
&.attachment--opengraph {
max-width: 800px;
}
.attachment__content { .attachment__content {
border-radius: 4px; border-radius: 4px;
border-style: solid; border-style: solid;
@@ -68,11 +71,29 @@
&.attachment__container--danger { &.attachment__container--danger {
border-left-color: #e40303; border-left-color: #e40303;
} }
&.attachment__container--opengraph {
display: table;
table-layout: fixed;
width: 100%;
margin: 0;
padding-bottom: 13px;
div {
margin: 0;
}
}
.sitename { .sitename {
color: #A3A3A3; color: #A3A3A3;
} }
} }
.attachment__body__wrap {
&.attachment__body__wrap--opengraph {
display: table-cell;
width: 100%;
vertical-align: top;
}
}
.attachment__body { .attachment__body {
float: left; float: left;
overflow-x: auto; overflow-x: auto;
@@ -83,13 +104,11 @@
&.attachment__body--no_thumb { &.attachment__body--no_thumb {
width: 100%; width: 100%;
} }
.attachment__image { &.attachment__body--opengraph {
margin-bottom: 0; float: none;
max-height: 150px; padding-right: 0;
max-width: 150px; width: 100%;
&.loading { word-wrap: break-word;
height: 150px;
}
} }
} }
@@ -97,10 +116,38 @@
display: inline-block; display: inline-block;
} }
.attachment__image__container--openraph {
display: table-cell;
vertical-align: top;
padding-top: 3px;
padding-left: 15px;
}
.attachment__image { .attachment__image {
margin-bottom: 1em; margin-bottom: 1em;
max-height: 300px; max-height: 300px;
max-width: 500px; 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 { .attachment__author-name {
@@ -121,6 +168,14 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
&.attachment__title--opengraph {
height: auto;
word-wrap: break-word;
&.is-url {
word-break: break-all
}
}
} }
.attachment-link-more { .attachment-link-more {

Просмотреть файл

@@ -1210,6 +1210,15 @@
} }
} }
} }
.post {
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-width: 200px;
}
}
}
}
} }
@media screen and (max-width: 640px) { @media screen and (max-width: 640px) {
@@ -1385,6 +1394,16 @@
text-align: left; text-align: left;
} }
} }
.post {
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-width: 200px;
}
}
}
}
} }
@media screen and (max-width: 550px) { @media screen and (max-width: 550px) {
@@ -1415,6 +1434,15 @@
top: 60px; top: 60px;
width: calc(100% - 30px); width: calc(100% - 30px);
} }
.post {
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-width: 180px;
}
}
}
}
} }
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
@@ -1521,6 +1549,16 @@
.integration__icon { .integration__icon {
display: none; display: none;
} }
.post {
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-width: 120px;
}
}
}
}
} }
@media screen and (max-height: 640px) { @media screen and (max-height: 640px) {
@@ -1553,6 +1591,13 @@
} }
} }
} }
.attachment {
.attachment__image {
&.attachment__image--openraph {
max-width: 80px;
}
}
}
} }
.tutorial-steps__container { .tutorial-steps__container {

Просмотреть файл

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

Просмотреть файл

@@ -24,12 +24,10 @@ describe('CommonUtils.getNearestPoint', function() {
nearestPointLte: {x: 1, y: 1} 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(nearestPoint.x, data.nearestPoint.x);
assert.equal(nearestPointData.nearestPoint.y, data.nearestPoint.y); assert.equal(nearestPoint.y, data.nearestPoint.y);
assert.equal(nearestPointData.nearestPointLte.x, data.nearestPointLte.x);
assert.equal(nearestPointData.nearestPointLte.y, data.nearestPointLte.y);
} }
}); });
}); });

Просмотреть файл

@@ -8,7 +8,6 @@ export function getDistanceBW2Points(point1, point2, xAttr = 'x', yAttr = 'y') {
*/ */
export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') { export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') {
var nearestPoint = {}; var nearestPoint = {};
var nearestPointLte = {}; // Nearest point smaller than or equal to point
for (const point of points) { for (const point of points) {
if (typeof nearestPoint[xAttr] === 'undefined' || typeof nearestPoint[yAttr] === 'undefined') { if (typeof nearestPoint[xAttr] === 'undefined' || typeof nearestPoint[yAttr] === 'undefined') {
nearestPoint = point; nearestPoint = point;
@@ -16,21 +15,6 @@ export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') {
// Check for bestImage // Check for bestImage
nearestPoint = point; nearestPoint = point;
} }
if (typeof nearestPointLte[xAttr] === 'undefined' || typeof nearestPointLte[yAttr] === 'undefined') {
if (point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr]) {
nearestPointLte = point;
} }
} else if ( return nearestPoint;
// 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
};
} }