From 3aaf71fdea914af1a7f2b2fb97bb6ae44132fcc4 Mon Sep 17 00:00:00 2001 From: Debanshu Kundu Date: Fri, 20 Jan 2017 23:11:13 +0530 Subject: #4257 Added functionality to create previews for post links using open graph data from those links. (#4890) --- webapp/components/post_view/components/post.jsx | 4 +- .../components/post_attachment_oembed.jsx | 108 ------ .../components/post_attachment_opengraph.jsx | 212 ++++++++++++ .../components/post_view/components/post_body.jsx | 4 +- .../components/post_body_additional_content.jsx | 64 ++-- .../components/post_view/components/post_list.jsx | 16 +- .../components/post_view/components/providers.json | 376 --------------------- 7 files changed, 253 insertions(+), 531 deletions(-) delete mode 100644 webapp/components/post_view/components/post_attachment_oembed.jsx create mode 100644 webapp/components/post_view/components/post_attachment_opengraph.jsx delete mode 100644 webapp/components/post_view/components/providers.json (limited to 'webapp/components') diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index 8ba3438a0..896002a6c 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -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} /> @@ -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 }; diff --git a/webapp/components/post_view/components/post_attachment_oembed.jsx b/webapp/components/post_view/components/post_attachment_oembed.jsx deleted file mode 100644 index 359c7cc35..000000000 --- a/webapp/components/post_view/components/post_attachment_oembed.jsx +++ /dev/null @@ -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 =
; - } else { - data = this.state.data; - content = ( -
- ); - } - - return ( -
-
-
-

- - {data.title} - -

-
-
- {content} -
-
-
-
-
- ); - } -} - -PostAttachmentOEmbed.propTypes = { - link: React.PropTypes.string.isRequired, - provider: React.PropTypes.object.isRequired -}; diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx new file mode 100644 index 000000000..20beaed51 --- /dev/null +++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx @@ -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 ( + + ); + } + return null; + } + + imageTag(imageUrl) { + if (imageUrl && this.state.imageVisible) { + return ( + + ); + } + 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 ( +
+
+
+ {data.site_name} +

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

+
+
+
+
+ {description}   + {this.imageToggleAnchoreTag(imageUrl)} +
+ {this.imageTag(imageUrl)} +
+
+
+
+
+
+ ); + } +} + +PostAttachmentOpenGraph.defaultProps = { + previewCollapsed: 'false' +}; + +PostAttachmentOpenGraph.propTypes = { + link: React.PropTypes.string.isRequired, + childComponentDidUpdateFunction: React.PropTypes.func, + previewCollapsed: React.PropTypes.string +}; diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx index 60e682e8d..10c24aab2 100644 --- a/webapp/components/post_view/components/post_body.jsx +++ b/webapp/components/post_view/components/post_body.jsx @@ -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 }; diff --git a/webapp/components/post_view/components/post_body_additional_content.jsx b/webapp/components/post_view/components/post_body_additional_content.jsx index e6c1f3b06..cad618de0 100644 --- a/webapp/components/post_view/components/post_body_additional_content.jsx +++ b/webapp/components/post_view/components/post_body_additional_content.jsx @@ -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 ( - - ); - } + if (link && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) { + return ( + + ); } return null; } render() { - const staticEmbed = this.generateStaticEmbed(); - - if (staticEmbed) { - return ( -
- {this.props.message} - {staticEmbed} -
- ); - } - 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 ( +
+ {this.props.message} + {staticEmbed} +
+ ); + } + 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 }; diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index e3724b688..7550db348 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -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() { diff --git a/webapp/components/post_view/components/providers.json b/webapp/components/post_view/components/providers.json deleted file mode 100644 index b5899c225..000000000 --- a/webapp/components/post_view/components/providers.json +++ /dev/null @@ -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 - } -] -- cgit v1.2.3-1-g7c22