diff options
Diffstat (limited to 'webapp')
-rw-r--r-- | webapp/actions/global_actions.jsx | 24 | ||||
-rw-r--r-- | webapp/client/client.jsx | 10 | ||||
-rw-r--r-- | webapp/components/post_view/components/post.jsx | 4 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_attachment_oembed.jsx | 108 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_attachment_opengraph.jsx | 212 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_body.jsx | 4 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_body_additional_content.jsx | 64 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_list.jsx | 16 | ||||
-rw-r--r-- | webapp/components/post_view/components/providers.json | 376 | ||||
-rw-r--r-- | webapp/sass/layout/_webhooks.scss | 20 | ||||
-rw-r--r-- | webapp/stores/opengraph_store.jsx | 68 | ||||
-rw-r--r-- | webapp/tests/utils_get_nearest_point.test.jsx | 35 | ||||
-rw-r--r-- | webapp/utils/commons.jsx | 36 | ||||
-rw-r--r-- | webapp/utils/constants.jsx | 3 | ||||
-rw-r--r-- | webapp/utils/utils.jsx | 12 |
15 files changed, 460 insertions, 532 deletions
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx index 4cfcaa6cb..23e19f22f 100644 --- a/webapp/actions/global_actions.jsx +++ b/webapp/actions/global_actions.jsx @@ -596,3 +596,27 @@ export function redirectUserToDefaultTeam() { browserHistory.push('/select_team'); } } + +requestOpenGraphMetadata.openGraphMetadataOnGoingRequests = {}; // Format: {<url>: true} +export function requestOpenGraphMetadata(url) { + const onself = requestOpenGraphMetadata; + + if (!onself.openGraphMetadataOnGoingRequests[url]) { + onself.openGraphMetadataOnGoingRequests[url] = true; + + Client.getOpenGraphMetadata(url, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIVED_OPEN_GRAPH_METADATA, + url, + data + }); + delete onself.openGraphMetadataOnGoingRequests[url]; + }, + (err) => { + AsyncClient.dispatchError(err, 'getOpenGraphMetadata'); + delete onself.openGraphMetadataOnGoingRequests[url]; + } + ); + } +} diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 639f2da2e..9f1bc926d 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1767,6 +1767,16 @@ export default class Client { end(this.handleResponse.bind(this, 'getFileInfosForPost', success, error)); } + getOpenGraphMetadata(url, success, error) { + request. + post(`${this.getBaseRoute()}/get_opengraph_metadata`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send({url}). + end(this.handleResponse.bind(this, 'getOpenGraphMetadata', success, error)); + } + // Routes for Files uploadFile(file, filename, channelId, clientId, success, error) { 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} /> </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 }; 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 = <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 -}; 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 ( + <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} + {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 +}; 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 ( - <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 }; 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 - } -] diff --git a/webapp/sass/layout/_webhooks.scss b/webapp/sass/layout/_webhooks.scss index 99a82f00e..904c50ccc 100644 --- a/webapp/sass/layout/_webhooks.scss +++ b/webapp/sass/layout/_webhooks.scss @@ -68,6 +68,9 @@ &.attachment__container--danger { border-left-color: #e40303; } + .sitename { + color: #A3A3A3; + } } .attachment__body { @@ -80,6 +83,14 @@ &.attachment__body--no_thumb { width: 100%; } + .attachment__image { + margin-bottom: 0; + max-height: 150px; + max-width: 150px; + &.loading { + height: 150px; + } + } } .attachment__text p:last-of-type { @@ -103,6 +114,13 @@ line-height: 18px; margin: 5px 0; padding: 0; + + &.has-link { + color: #2f81b7; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } } .attachment-link-more { @@ -144,4 +162,4 @@ } } } -}
\ No newline at end of file +} diff --git a/webapp/stores/opengraph_store.jsx b/webapp/stores/opengraph_store.jsx new file mode 100644 index 000000000..4ad156df0 --- /dev/null +++ b/webapp/stores/opengraph_store.jsx @@ -0,0 +1,68 @@ +import EventEmitter from 'events'; + +import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; + +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; +const URL_DATA_CHANGE_EVENT = 'url_data_change'; + +class OpenGraphStoreClass extends EventEmitter { + constructor() { + super(); + this.ogDataObject = {}; // Format: {<url>: <data-object>} + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + emitUrlDataChange(url) { + this.emit(URL_DATA_CHANGE_EVENT, url); + } + + addUrlDataChangeListener(callback) { + this.on(URL_DATA_CHANGE_EVENT, callback); + } + + removeUrlDataChangeListener(callback) { + this.removeListener(URL_DATA_CHANGE_EVENT, callback); + } + + storeOgInfo(url, ogInfo) { + this.ogDataObject[url] = ogInfo; + } + + getOgInfo(url) { + return this.ogDataObject[url]; + } +} + +var OpenGraphStore = new OpenGraphStoreClass(); + +// Not expecting more that `Constants.POST_CHUNK_SIZE` post previews rendered at a time +OpenGraphStore.setMaxListeners(Constants.POST_CHUNK_SIZE); + +OpenGraphStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECIVED_OPEN_GRAPH_METADATA: + OpenGraphStore.storeOgInfo(action.url, action.data); + OpenGraphStore.emitUrlDataChange(action.url); + OpenGraphStore.emitChange(); + break; + default: + } +}); + +export default OpenGraphStore; diff --git a/webapp/tests/utils_get_nearest_point.test.jsx b/webapp/tests/utils_get_nearest_point.test.jsx new file mode 100644 index 000000000..b0b0a2e0e --- /dev/null +++ b/webapp/tests/utils_get_nearest_point.test.jsx @@ -0,0 +1,35 @@ +import assert from 'assert'; +import * as CommonUtils from 'utils/commons.jsx'; + +describe('CommonUtils.getNearestPoint', function() { + this.timeout(10000); + it('should return nearest point', function() { + for (const data of [ + { + points: [{x: 30, y: 40}, {x: 50, y: 50}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}, {x: 10, y: 20}], + pivotPoint: {x: 10, y: 20}, + nearestPoint: {x: 10, y: 20}, + nearestPointLte: {x: 10, y: 20} + }, + { + points: [{x: 50, y: 50}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}, {x: 100, y: 90}, {x: 30, y: 40}], + pivotPoint: {x: 10, y: 20}, + nearestPoint: {x: 30, y: 40}, + nearestPointLte: {} + }, + { + points: [{x: 50, y: 50}, {x: 1, y: 1}, {x: 15, y: 25}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}], + pivotPoint: {x: 10, y: 20}, + nearestPoint: {x: 15, y: 25}, + nearestPointLte: {x: 1, y: 1} + } + ]) { + const nearestPointData = 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); + } + }); +}); diff --git a/webapp/utils/commons.jsx b/webapp/utils/commons.jsx new file mode 100644 index 000000000..1888869dc --- /dev/null +++ b/webapp/utils/commons.jsx @@ -0,0 +1,36 @@ +export function getDistanceBW2Points(point1, point2, xAttr = 'x', yAttr = 'y') { + return Math.sqrt(Math.pow(point1[xAttr] - point2[xAttr], 2) + Math.pow(point1[yAttr] - point2[yAttr], 2)); +} + +/** + * Funtion to return nearest point of given pivot point. + * It return two points one nearest and other nearest but having both coorditanes smaller than the given point's coordinates. + */ +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; + } else if (getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPoint, pivotPoint, xAttr, yAttr)) { + // 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 + }; +} diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 6377f27f2..b1c188d89 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -146,6 +146,9 @@ export const ActionTypes = keyMirror({ RECEIVED_LOCALE: null, + UPDATE_OPEN_GRAPH_METADATA: null, + RECIVED_OPEN_GRAPH_METADATA: null, + SHOW_SEARCH: null, USER_TYPING: null, diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 9654ff605..a0aecbdb3 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -1324,3 +1324,15 @@ export function handleFormattedTextClick(e) { browserHistory.push('/' + TeamStore.getCurrent().name + '/channels/' + channelMentionAttribute.value); } } + +export function isEmptyObject(object) { + if (!object) { + return true; + } + + if (Object.keys(object).length === 0) { + return true; + } + + return false; +} |