-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix #463 - Fetch and display previews of URLs using OpenGraph tags
- Loading branch information
Showing
26 changed files
with
302 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import api from '../api'; | ||
|
||
export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST'; | ||
export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS'; | ||
export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL'; | ||
|
||
export function fetchStatusCard(id) { | ||
return (dispatch, getState) => { | ||
dispatch(fetchStatusCardRequest(id)); | ||
|
||
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { | ||
dispatch(fetchStatusCardSuccess(id, response.data)); | ||
}).catch(error => { | ||
dispatch(fetchStatusCardFail(id, error)); | ||
}); | ||
}; | ||
}; | ||
|
||
export function fetchStatusCardRequest(id) { | ||
return { | ||
type: STATUS_CARD_FETCH_REQUEST, | ||
id | ||
}; | ||
}; | ||
|
||
export function fetchStatusCardSuccess(id, card) { | ||
return { | ||
type: STATUS_CARD_FETCH_SUCCESS, | ||
id, | ||
card | ||
}; | ||
}; | ||
|
||
export function fetchStatusCardFail(id, error) { | ||
return { | ||
type: STATUS_CARD_FETCH_FAIL, | ||
id, | ||
error | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
app/assets/javascripts/components/features/status/components/card.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | ||
import ImmutablePropTypes from 'react-immutable-proptypes'; | ||
|
||
const outerStyle = { | ||
display: 'flex', | ||
cursor: 'pointer', | ||
fontSize: '14px', | ||
border: '1px solid #363c4b', | ||
borderRadius: '4px', | ||
color: '#616b86', | ||
marginTop: '14px', | ||
textDecoration: 'none', | ||
overflow: 'hidden' | ||
}; | ||
|
||
const contentStyle = { | ||
flex: '2', | ||
padding: '8px', | ||
paddingLeft: '14px' | ||
}; | ||
|
||
const titleStyle = { | ||
display: 'block', | ||
fontWeight: '500', | ||
marginBottom: '5px', | ||
color: '#d9e1e8' | ||
}; | ||
|
||
const descriptionStyle = { | ||
color: '#d9e1e8' | ||
}; | ||
|
||
const imageOuterStyle = { | ||
flex: '1', | ||
background: '#373b4a' | ||
}; | ||
|
||
const imageStyle = { | ||
display: 'block', | ||
width: '100%', | ||
height: 'auto', | ||
margin: '0', | ||
borderRadius: '4px 0 0 4px' | ||
}; | ||
|
||
const hostStyle = { | ||
display: 'block', | ||
marginTop: '5px', | ||
fontSize: '13px' | ||
}; | ||
|
||
const getHostname = url => { | ||
const parser = document.createElement('a'); | ||
parser.href = url; | ||
return parser.hostname; | ||
}; | ||
|
||
const Card = React.createClass({ | ||
propTypes: { | ||
card: ImmutablePropTypes.map | ||
}, | ||
|
||
mixins: [PureRenderMixin], | ||
|
||
render () { | ||
const { card } = this.props; | ||
|
||
if (card === null) { | ||
return null; | ||
} | ||
|
||
let image = ''; | ||
|
||
if (card.get('image')) { | ||
image = ( | ||
<div style={imageOuterStyle}> | ||
<img src={card.get('image')} alt={card.get('title')} style={imageStyle} /> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<a style={outerStyle} href={card.get('url')} className='status-card'> | ||
{image} | ||
|
||
<div style={contentStyle}> | ||
<strong style={titleStyle}>{card.get('title')}</strong> | ||
<p style={descriptionStyle}>{card.get('description')}</p> | ||
<span style={hostStyle}>{getHostname(card.get('url'))}</span> | ||
</div> | ||
</a> | ||
); | ||
} | ||
}); | ||
|
||
export default Card; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
app/assets/javascripts/components/features/status/containers/card_container.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { connect } from 'react-redux'; | ||
import Card from '../components/card'; | ||
|
||
const mapStateToProps = (state, { statusId }) => ({ | ||
card: state.getIn(['cards', statusId], null) | ||
}); | ||
|
||
export default connect(mapStateToProps)(Card); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; | ||
|
||
import Immutable from 'immutable'; | ||
|
||
const initialState = Immutable.Map(); | ||
|
||
export default function cards(state = initialState, action) { | ||
switch(action.type) { | ||
case STATUS_CARD_FETCH_SUCCESS: | ||
return state.set(action.id, Immutable.fromJS(action.card)); | ||
default: | ||
return state; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# frozen_string_literal: true | ||
|
||
class PreviewCard < ApplicationRecord | ||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze | ||
|
||
belongs_to :status | ||
|
||
has_attached_file :image, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } | ||
|
||
validates :url, presence: true | ||
validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES | ||
validates_attachment_size :image, less_than: 1.megabytes | ||
|
||
def save_with_optional_image! | ||
save! | ||
rescue ActiveRecord::RecordInvalid | ||
self.image = nil | ||
save! | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# frozen_string_literal: true | ||
|
||
class FetchLinkCardService < BaseService | ||
def call(status) | ||
# Get first URL | ||
url = URI.extract(status.text).reject { |uri| (uri =~ /\Ahttps?:\/\//).nil? }.first | ||
|
||
return if url.nil? | ||
|
||
response = http_client.get(url) | ||
|
||
return if response.code != 200 | ||
|
||
page = Nokogiri::HTML(response.to_s) | ||
card = PreviewCard.where(status: status).first_or_initialize(status: status, url: url) | ||
|
||
card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content | ||
card.description = meta_property(page, 'og:description') || meta_property(page, 'description') | ||
card.image = URI.parse(meta_property(page, 'og:image')) if meta_property(page, 'og:image') | ||
|
||
card.save_with_optional_image! | ||
end | ||
|
||
private | ||
|
||
def http_client | ||
HTTP.timeout(:per_operation, write: 10, connect: 10, read: 10).follow | ||
end | ||
|
||
def meta_property(html, property) | ||
html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
object @card | ||
|
||
attributes :url, :title, :description | ||
|
||
node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# frozen_string_literal: true | ||
|
||
class LinkCrawlWorker | ||
include Sidekiq::Worker | ||
|
||
sidekiq_options retry: false | ||
|
||
def perform(status_id) | ||
FetchLinkCardService.new.call(Status.find(status_id)) | ||
rescue ActiveRecord::RecordNotFound | ||
true | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,5 @@ | |
|
||
ActiveSupport::Inflector.inflections(:en) do |inflect| | ||
inflect.acronym 'StatsD' | ||
inflect.acronym 'OEmbed' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.