#4257 Added functionality to create previews for post links using open graph data from those links. (#4890)
Этот коммит содержится в:
коммит произвёл
enahum
родитель
fefe4b70d9
Коммит
3aaf71fdea
@@ -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
|
||||
};
|
||||
212
webapp/components/post_view/components/post_attachment_opengraph.jsx
Обычный файл
212
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
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
Ссылка в новой задаче
Block a user