#4257 Added functionality to create previews for post links using open graph data from those links. (#4890)

Этот коммит содержится в:
Debanshu Kundu
2017-01-20 23:11:13 +05:30
коммит произвёл enahum
родитель fefe4b70d9
Коммит 3aaf71fdea
26 изменённых файлов: 1241 добавлений и 534 удалений

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

@@ -289,6 +289,7 @@ export default class Post extends React.Component {
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
isCommentMention={this.props.isCommentMention}
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
</div>
</div>
@@ -317,5 +318,6 @@ Post.propTypes = {
useMilitaryTime: React.PropTypes.bool.isRequired,
isFlagged: React.PropTypes.bool,
status: React.PropTypes.string,
isBusy: React.PropTypes.bool
isBusy: React.PropTypes.bool,
childComponentDidUpdateFunction: React.PropTypes.func
};

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

@@ -1,108 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import React from 'react';
export default class PostAttachmentOEmbed extends React.Component {
constructor(props) {
super(props);
this.fetchData = this.fetchData.bind(this);
this.isLoading = false;
}
componentWillMount() {
this.setState({data: {}});
}
componentWillReceiveProps(nextProps) {
if (nextProps.link !== this.props.link) {
this.isLoading = false;
this.fetchData(nextProps.link);
}
}
componentDidMount() {
this.fetchData(this.props.link);
}
fetchData(link) {
if (!this.isLoading) {
this.isLoading = true;
let url = 'https://noembed.com/embed?nowrap=on';
url += '&url=' + encodeURIComponent(link);
url += '&maxheight=' + this.props.provider.height;
return $.ajax({
url,
dataType: 'jsonp',
success: (result) => {
this.isLoading = false;
if (result.error) {
this.setState({data: {}});
} else {
this.setState({data: result});
}
},
error: () => {
this.setState({data: {}});
}
});
}
return null;
}
render() {
let data = {};
let content;
if ($.isEmptyObject(this.state.data)) {
content = <div style={{height: this.props.provider.height}}/>;
} else {
data = this.state.data;
content = (
<div
style={{height: this.props.provider.height}}
dangerouslySetInnerHTML={{__html: data.html}}
/>
);
}
return (
<div
className='attachment attachment--oembed'
ref='attachment'
>
<div className='attachment__content'>
<div
className={'clearfix attachment__container'}
>
<h1
className='attachment__title'
>
<a
className='attachment__title-link'
href={data.url}
target='_blank'
rel='noopener noreferrer'
>
{data.title}
</a>
</h1>
<div >
<div
className={'attachment__body attachment__body--no_thumb'}
>
{content}
</div>
</div>
</div>
</div>
</div>
);
}
}
PostAttachmentOEmbed.propTypes = {
link: React.PropTypes.string.isRequired,
provider: React.PropTypes.object.isRequired
};

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

@@ -0,0 +1,212 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import OpenGraphStore from 'stores/opengraph_store.jsx';
import * as Utils from 'utils/utils.jsx';
import * as CommonUtils from 'utils/commons.jsx';
import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
export default class PostAttachmentOpenGraph extends React.Component {
constructor(props) {
super(props);
this.imageDimentions = { // Image dimentions in pixels.
height: 150,
width: 150
};
this.maxDescriptionLength = 300;
this.descriptionEllipsis = '...';
this.fetchData = this.fetchData.bind(this);
this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
this.onImageLoad = this.onImageLoad.bind(this);
}
componentWillMount() {
this.setState({
data: {},
imageLoaded: false,
imageVisible: this.props.previewCollapsed.startsWith('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);
}
}
shouldComponentUpdate(nextProps, nextState) {
if (nextState.imageVisible !== this.state.imageVisible) {
return true;
}
if (nextState.imageLoaded !== this.state.imageLoaded) {
return true;
}
if (!Utils.areObjectsEqual(nextState.data, this.state.data)) {
return true;
}
return false;
}
componentDidMount() {
OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
}
componentDidUpdate() {
if (this.props.childComponentDidUpdateFunction) {
this.props.childComponentDidUpdateFunction();
}
}
componentWillUnmount() {
OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
}
onOpenGraphMetadataChange(url) {
if (url === this.props.link) {
this.fetchData(url);
}
}
fetchData(url) {
const data = OpenGraphStore.getOgInfo(url);
this.setState({data, imageLoaded: false});
if (Utils.isEmptyObject(data)) {
requestOpenGraphMetadata(url);
}
}
getBestImageUrl() {
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;
}
toggleImageVisibility() {
this.setState({imageVisible: !this.state.imageVisible});
}
onImageLoad() {
this.setState({imageLoaded: true});
}
loadImage(src) {
const img = new Image();
img.onload = this.onImageLoad;
img.src = src;
}
imageToggleAnchoreTag(imageUrl) {
if (imageUrl) {
return (
<a
className={'post__embed-visibility'}
data-expanded={this.state.imageVisible}
aria-label='Toggle Embed Visibility'
onClick={this.toggleImageVisibility}
/>
);
}
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}
/>
);
}
return null;
}
render() {
if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
return null;
}
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) {
this.loadImage(imageUrl);
}
return (
<div
className='attachment attachment--oembed'
ref='attachment'
>
<div className='attachment__content'>
<div
className={'clearfix attachment__container'}
>
<span className='sitename'>{data.site_name}</span>
<h1
className='attachment__title has-link'
>
<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'}
>
<div>
<div>
{description} &nbsp;
{this.imageToggleAnchoreTag(imageUrl)}
</div>
{this.imageTag(imageUrl)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
PostAttachmentOpenGraph.defaultProps = {
previewCollapsed: 'false'
};
PostAttachmentOpenGraph.propTypes = {
link: React.PropTypes.string.isRequired,
childComponentDidUpdateFunction: React.PropTypes.func,
previewCollapsed: React.PropTypes.string
};

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

@@ -188,6 +188,7 @@ export default class PostBody extends React.Component {
message={messageWrapper}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
);
}
@@ -221,5 +222,6 @@ PostBody.propTypes = {
handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string,
isCommentMention: React.PropTypes.bool
isCommentMention: React.PropTypes.bool,
childComponentDidUpdateFunction: React.PropTypes.func
};

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

@@ -2,12 +2,11 @@
// See License.txt for license information.
import PostAttachmentList from './post_attachment_list.jsx';
import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
import PostImage from './post_image.jsx';
import YoutubeVideo from 'components/youtube_video.jsx';
import Constants from 'utils/constants.jsx';
import OEmbedProviders from './providers.json';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
@@ -17,7 +16,6 @@ export default class PostBodyAdditionalContent extends React.Component {
super(props);
this.getSlackAttachment = this.getSlackAttachment.bind(this);
this.getOEmbedProvider = this.getOEmbedProvider.bind(this);
this.generateToggleableEmbed = this.generateToggleableEmbed.bind(this);
this.generateStaticEmbed = this.generateStaticEmbed.bind(this);
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
@@ -72,18 +70,6 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
getOEmbedProvider(link) {
for (let i = 0; i < OEmbedProviders.length; i++) {
for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) {
if (link.match(OEmbedProviders[i].patterns[j])) {
return OEmbedProviders[i];
}
}
}
return null;
}
isLinkImage(link) {
const regex = /.+\/(.+\.(?:jpg|gif|bmp|png|jpeg))(?:\?.*)?$/i;
const match = link.match(regex);
@@ -152,38 +138,20 @@ export default class PostBodyAdditionalContent extends React.Component {
}
const link = Utils.extractFirstLink(this.props.post.message);
if (!link) {
return null;
}
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
const provider = this.getOEmbedProvider(link);
if (provider) {
return (
<PostAttachmentOEmbed
provider={provider}
link={link}
/>
);
}
if (link && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
return (
<PostAttachmentOpenGraph
link={link}
childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
previewCollapsed={this.props.previewCollapsed}
/>
);
}
return null;
}
render() {
const staticEmbed = this.generateStaticEmbed();
if (staticEmbed) {
return (
<div>
{this.props.message}
{staticEmbed}
</div>
);
}
if (this.isLinkToggleable() && !this.state.linkLoadError) {
const messageWithToggle = [];
@@ -224,6 +192,17 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
const staticEmbed = this.generateStaticEmbed();
if (staticEmbed) {
return (
<div>
{this.props.message}
{staticEmbed}
</div>
);
}
return this.props.message;
}
}
@@ -235,5 +214,6 @@ PostBodyAdditionalContent.propTypes = {
post: React.PropTypes.object.isRequired,
message: React.PropTypes.element.isRequired,
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string
previewCollapsed: React.PropTypes.string,
childComponentDidUpdateFunction: React.PropTypes.func
};

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

@@ -45,6 +45,7 @@ export default class PostList extends React.Component {
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.childComponentDidUpdate = this.childComponentDidUpdate.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -347,6 +348,7 @@ export default class PostList extends React.Component {
isFlagged={isFlagged}
status={status}
isBusy={this.props.isBusy}
childComponentDidUpdateFunction={this.childComponentDidUpdate}
/>
);
@@ -492,6 +494,12 @@ export default class PostList extends React.Component {
);
}
checkAndUpdateScrolling() {
if (this.props.postList != null && this.refs.postlist) {
this.updateScrolling();
}
}
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -509,9 +517,11 @@ export default class PostList extends React.Component {
}
componentDidUpdate() {
if (this.props.postList != null && this.refs.postlist) {
this.updateScrolling();
}
this.checkAndUpdateScrolling();
}
childComponentDidUpdate() {
this.checkAndUpdateScrolling();
}
render() {

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

@@ -1,376 +0,0 @@
[
{
"patterns": [
"http://(?:www\\.)?xkcd\\.com/\\d+/?"
],
"name": "XKCD",
"height": 110
},
{
"patterns": [
"https?://soundcloud.com/.*/.*"
],
"name": "SoundCloud",
"height": 140
},
{
"patterns": [
"https?://(?:www\\.)?flickr\\.com/.*",
"https?://flic\\.kr/p/[a-zA-Z0-9]+"
],
"name": "Flickr",
"height": 110
},
{
"patterns": [
"http://www\\.ted\\.com/talks/.+\\.html"
],
"name": "TED",
"height": 110
},
{
"patterns": [
"http://(?:www\\.)?theverge\\.com/\\d{4}/\\d{1,2}/\\d{1,2}/\\d+/[^/]+/?$"
],
"name": "The Verge",
"height": 110
},
{
"patterns": [
"http://.*\\.viddler\\.com/.*"
],
"name": "Viddler",
"height": 110
},
{
"patterns": [
"https?://(?:www\\.)?avclub\\.com/article/[^/]+/?$"
],
"name": "The AV Club",
"height": 110
},
{
"patterns": [
"https?://(?:www\\.)?wired\\.com/([^/]+/)?\\d+/\\d+/[^/]+/?$"
],
"name": "Wired",
"height": 110
},
{
"patterns": [
"http://www\\.theonion\\.com/articles/[^/]+/?"
],
"name": "The Onion",
"height": 110
},
{
"patterns": [
"http://yfrog\\.com/[0-9a-zA-Z]+/?$"
],
"name": "YFrog",
"height": 110
},
{
"patterns": [
"http://www\\.duffelblog\\.com/\\d{4}/\\d{1,2}/[^/]+/?$"
],
"name": "The Duffel Blog",
"height": 110
},
{
"patterns": [
"http://www\\.clickhole\\.com/article/[^/]+/?"
],
"name": "Clickhole",
"height": 110
},
{
"patterns": [
"https?://(?:www.)?skitch.com/([^/]+)/[^/]+/.+",
"http://skit.ch/[^/]+"
],
"name": "Skitch",
"height": 110
},
{
"patterns": [
"https?://(alpha|posts|photos)\\.app\\.net/.*"
],
"name": "ADN",
"height": 110
},
{
"patterns": [
"https?://gist\\.github\\.com/(?:[-0-9a-zA-Z]+/)?([0-9a-fA-f]+)"
],
"name": "Gist",
"height": 110
},
{
"patterns": [
"https?://www\\.(dropbox\\.com/s/.+\\.(?:jpg|png|gif))",
"https?://db\\.tt/[a-zA-Z0-9]+"
],
"name": "Dropbox",
"height": 110
},
{
"patterns": [
"https?://[^\\.]+\\.wikipedia\\.org/wiki/(?!Talk:)[^#]+(?:#(.+))?"
],
"name": "Wikipedia",
"height": 110
},
{
"patterns": [
"http://www.traileraddict.com/trailer/[^/]+/trailer"
],
"name": "TrailerAddict",
"height": 110
},
{
"patterns": [
"http://lockerz\\.com/[sd]/\\d+"
],
"name": "Lockerz",
"height": 110
},
{
"patterns": [
"http://gifuk\\.com/s/[0-9a-f]{16}"
],
"name": "GIFUK",
"height": 110
},
{
"patterns": [
"http://trailers\\.apple\\.com/trailers/[^/]+/[^/]+"
],
"name": "iTunes Movie Trailers",
"height": 110
},
{
"patterns": [
"http://gfycat\\.com/([a-zA-Z]+)"
],
"name": "Gfycat",
"height": 110
},
{
"patterns": [
"http://bash\\.org/\\?(\\d+)"
],
"name": "Bash.org",
"height": 110
},
{
"patterns": [
"http://arstechnica\\.com/[^/]+/\\d+/\\d+/[^/]+/?$"
],
"name": "Ars Technica",
"height": 110
},
{
"patterns": [
"http://imgur\\.com/gallery/[0-9a-zA-Z]+"
],
"name": "Imgur",
"height": 110
},
{
"patterns": [
"http://www\\.asciiartfarts\\.com/[0-9]+\\.html"
],
"name": "ASCII Art Farts",
"height": 110
},
{
"patterns": [
"http://www\\.monoprice\\.com/products/product\\.asp\\?.*p_id=\\d+"
],
"name": "Monoprice",
"height": 110
},
{
"patterns": [
"http://boingboing\\.net/\\d{4}/\\d{2}/\\d{2}/[^/]+\\.html"
],
"name": "Boing Boing",
"height": 110
},
{
"patterns": [
"https?://github\\.com/([^/]+)/([^/]+)/commit/(.+)",
"http://git\\.io/[_0-9a-zA-Z]+"
],
"name": "Github Commit",
"height": 110
},
{
"patterns": [
"https?://open\\.spotify\\.com/(track|album)/([0-9a-zA-Z]{22})"
],
"name": "Spotify",
"height": 110
},
{
"patterns": [
"https?://path\\.com/p/([0-9a-zA-Z]+)$"
],
"name": "Path",
"height": 110
},
{
"patterns": [
"http://www.funnyordie.com/videos/[^/]+/.+"
],
"name": "Funny or Die",
"height": 110
},
{
"patterns": [
"http://(?:www\\.)?twitpic\\.com/([^/]+)"
],
"name": "Twitpic",
"height": 110
},
{
"patterns": [
"https?://www\\.giantbomb\\.com/videos/[^/]+/\\d+-\\d+/?"
],
"name": "GiantBomb",
"height": 110
},
{
"patterns": [
"http://(?:www\\.)?beeradvocate\\.com/beer/profile/\\d+/\\d+"
],
"name": "Beer Advocate",
"height": 110
},
{
"patterns": [
"http://(?:www\\.)?imdb.com/title/(tt\\d+)"
],
"name": "IMDB",
"height": 110
},
{
"patterns": [
"http://cl\\.ly/(?:image/)?[0-9a-zA-Z]+/?$"
],
"name": "CloudApp",
"height": 110
},
{
"patterns": [
"http://clyp\\.it/.*"
],
"name": "Clyp",
"height": 110
},
{
"patterns": [
"http://www\\.hulu\\.com/watch/.*"
],
"name": "Hulu",
"height": 110
},
{
"patterns": [
"https?://(?:www|mobile\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/?$",
"https?://t\\.co/[a-zA-Z0-9]+"
],
"name": "Twitter",
"height": 110
},
{
"patterns": [
"https?://(?:www\\.)?vimeo\\.com/.+"
],
"name": "Vimeo",
"height": 110
},
{
"patterns": [
"http://www\\.amazon\\.com/(?:.+/)?[gd]p/(?:product/)?(?:tags-on-product/)?([a-zA-Z0-9]+)",
"http://amzn\\.com/([^/]+)"
],
"name": "Amazon",
"height": 110
},
{
"patterns": [
"http://qik\\.com/video/.*"
],
"name": "Qik",
"height": 110
},
{
"patterns": [
"http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/?",
"http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/track/[^/]+/?",
"http://www\\.rdio\\.com/people/[^/]+/playlists/\\d+/[^/]+"
],
"name": "Rdio",
"height": 110
},
{
"patterns": [
"http://www\\.slideshare\\.net/.*/.*"
],
"name": "SlideShare",
"height": 110
},
{
"patterns": [
"http://imgur\\.com/([0-9a-zA-Z]+)$"
],
"name": "Imgur",
"height": 110
},
{
"patterns": [
"https?://instagr(?:\\.am|am\\.com)/p/.+"
],
"name": "Instagram",
"height": 110
},
{
"patterns": [
"http://www\\.twitlonger\\.com/show/[a-zA-Z0-9]+",
"http://tl\\.gd/[^/]+"
],
"name": "Twitlonger",
"height": 110
},
{
"patterns": [
"https?://vine.co/v/[a-zA-Z0-9]+"
],
"name": "Vine",
"height": 490
},
{
"patterns": [
"http://www\\.urbandictionary\\.com/define\\.php\\?term=.+"
],
"name": "Urban Dictionary",
"height": 110
},
{
"patterns": [
"http://picplz\\.com/user/[^/]+/pic/[^/]+"
],
"name": "Picplz",
"height": 110
},
{
"patterns": [
"https?://(?:www\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/photo/\\d+(?:/large|/)?$",
"https?://pic\\.twitter\\.com/.+"
],
"name": "Twitter",
"height": 110
}
]