summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
Diffstat (limited to 'webapp')
-rw-r--r--webapp/actions/global_actions.jsx24
-rw-r--r--webapp/client/client.jsx10
-rw-r--r--webapp/components/post_view/components/post.jsx4
-rw-r--r--webapp/components/post_view/components/post_attachment_oembed.jsx108
-rw-r--r--webapp/components/post_view/components/post_attachment_opengraph.jsx212
-rw-r--r--webapp/components/post_view/components/post_body.jsx4
-rw-r--r--webapp/components/post_view/components/post_body_additional_content.jsx64
-rw-r--r--webapp/components/post_view/components/post_list.jsx16
-rw-r--r--webapp/components/post_view/components/providers.json376
-rw-r--r--webapp/sass/layout/_webhooks.scss20
-rw-r--r--webapp/stores/opengraph_store.jsx68
-rw-r--r--webapp/tests/utils_get_nearest_point.test.jsx35
-rw-r--r--webapp/utils/commons.jsx36
-rw-r--r--webapp/utils/constants.jsx3
-rw-r--r--webapp/utils/utils.jsx12
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} &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
+};
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;
+}