Change to treat the url to a post already owned as an internal link
This commit is contained in:
parent
ff063001d5
commit
bd183630a4
5 changed files with 73 additions and 2 deletions
|
@ -1,15 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PollContainer from 'mastodon/containers/poll_container';
|
import PollContainer from 'mastodon/containers/poll_container';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import { autoPlayGif, show_reply_tree_button } from 'mastodon/initial_state';
|
import { autoPlayGif, show_reply_tree_button } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
postByAcct: { id: 'status.post_by_acct', defaultMessage: 'Post by @{acct}' },
|
||||||
|
});
|
||||||
|
|
||||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
|
@injectIntl
|
||||||
export default class StatusContent extends React.PureComponent {
|
export default class StatusContent extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -25,6 +30,7 @@ export default class StatusContent extends React.PureComponent {
|
||||||
collapsable: PropTypes.bool,
|
collapsable: PropTypes.bool,
|
||||||
onCollapsedToggle: PropTypes.func,
|
onCollapsedToggle: PropTypes.func,
|
||||||
quote: PropTypes.bool,
|
quote: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -58,6 +64,9 @@ export default class StatusContent extends React.PureComponent {
|
||||||
link.setAttribute('title', mention.get('acct'));
|
link.setAttribute('title', mention.get('acct'));
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||||
|
} else if (link.classList.contains('status-url-link')) {
|
||||||
|
link.setAttribute('title', this.props.intl.formatMessage(messages.postByAcct, { acct: link.dataset.statusAccountAcct }));
|
||||||
|
link.addEventListener('click', this.onStatusUrlClick.bind(this, link.dataset.statusId), false);
|
||||||
} else {
|
} else {
|
||||||
link.setAttribute('title', link.href);
|
link.setAttribute('title', link.href);
|
||||||
link.classList.add('unhandled-link');
|
link.classList.add('unhandled-link');
|
||||||
|
@ -137,6 +146,13 @@ export default class StatusContent extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onStatusUrlClick = (statusId, e) => {
|
||||||
|
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.context.router.history.push(`/statuses/${statusId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onQuoteClick = (statusId, e) => {
|
onQuoteClick = (statusId, e) => {
|
||||||
let statusUrl = `/statuses/${statusId}`;
|
let statusUrl = `/statuses/${statusId}`;
|
||||||
|
|
||||||
|
|
|
@ -510,6 +510,7 @@
|
||||||
"status.open": "Expand this post",
|
"status.open": "Expand this post",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pin on profile",
|
||||||
"status.pinned": "Pinned post",
|
"status.pinned": "Pinned post",
|
||||||
|
"status.post_by_acct": "Post by @{acct}",
|
||||||
"status.quote": "Quote",
|
"status.quote": "Quote",
|
||||||
"status.read_more": "Read more",
|
"status.read_more": "Read more",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
|
|
|
@ -511,6 +511,7 @@
|
||||||
"status.open": "詳細を表示",
|
"status.open": "詳細を表示",
|
||||||
"status.pin": "プロフィールに固定表示",
|
"status.pin": "プロフィールに固定表示",
|
||||||
"status.pinned": "固定された投稿",
|
"status.pinned": "固定された投稿",
|
||||||
|
"status.post_by_acct": "@{acct}による投稿",
|
||||||
"status.quote": "引用",
|
"status.quote": "引用",
|
||||||
"status.read_more": "もっと見る",
|
"status.read_more": "もっと見る",
|
||||||
"status.reblog": "ブースト",
|
"status.reblog": "ブースト",
|
||||||
|
|
|
@ -6,6 +6,7 @@ class EntityCache
|
||||||
include Singleton
|
include Singleton
|
||||||
|
|
||||||
MAX_EXPIRATION = 7.days.freeze
|
MAX_EXPIRATION = 7.days.freeze
|
||||||
|
MIN_EXPIRATION = 60.seconds.freeze
|
||||||
|
|
||||||
def status(url)
|
def status(url)
|
||||||
Rails.cache.fetch(to_key(:status, url), expires_in: MAX_EXPIRATION) { FetchRemoteStatusService.new.call(url) }
|
Rails.cache.fetch(to_key(:status, url), expires_in: MAX_EXPIRATION) { FetchRemoteStatusService.new.call(url) }
|
||||||
|
@ -34,6 +35,26 @@ class EntityCache
|
||||||
shortcodes.filter_map { |shortcode| cached[to_key(:emoji, shortcode, domain)] || uncached[shortcode] }
|
shortcodes.filter_map { |shortcode| cached[to_key(:emoji, shortcode, domain)] || uncached[shortcode] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def holding_status_and_account(url)
|
||||||
|
return Rails.cache.read(to_key(:holding_status, url)) if Rails.cache.exist?(to_key(:holding_status, url))
|
||||||
|
|
||||||
|
status = begin
|
||||||
|
if ActivityPub::TagManager.instance.local_uri?(url)
|
||||||
|
StatusFinder.new(url).status
|
||||||
|
else
|
||||||
|
Status.where(uri: url).or(Status.where(url: url)).first
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
account = status&.account
|
||||||
|
|
||||||
|
Rails.cache.write(to_key(:holding_status, url), [status, account], expires_in: account.nil? ? MIN_EXPIRATION : MAX_EXPIRATION)
|
||||||
|
|
||||||
|
[status, account]
|
||||||
|
end
|
||||||
|
|
||||||
def to_key(type, *ids)
|
def to_key(type, *ids)
|
||||||
"#{type}:#{ids.compact.map(&:downcase).join(':')}"
|
"#{type}:#{ids.compact.map(&:downcase).join(':')}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Formatter
|
||||||
|
|
||||||
unless status.local?
|
unless status.local?
|
||||||
html = reformat(raw_content)
|
html = reformat(raw_content)
|
||||||
|
html = apply_inner_link(html)
|
||||||
html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
|
html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
|
||||||
return html.html_safe # rubocop:disable Rails/OutputSafety
|
return html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
end
|
end
|
||||||
|
@ -52,7 +53,7 @@ class Formatter
|
||||||
html.sub!(/^<p>(.+)<\/p>$/, '\1')
|
html.sub!(/^<p>(.+)<\/p>$/, '\1')
|
||||||
html = Sanitize.clean(html).delete("\n").truncate(150)
|
html = Sanitize.clean(html).delete("\n").truncate(150)
|
||||||
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
|
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
|
||||||
html.html_safe
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
end
|
end
|
||||||
|
|
||||||
def reformat(html)
|
def reformat(html)
|
||||||
|
@ -248,11 +249,42 @@ class Formatter
|
||||||
|
|
||||||
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
||||||
|
|
||||||
|
status, account = url_to_holding_status_and_account(url.normalize.to_s)
|
||||||
|
|
||||||
|
if account.present?
|
||||||
|
html_attrs[:class] = 'status-url-link'
|
||||||
|
html_attrs[:'data-status-id'] = status.id
|
||||||
|
html_attrs[:'data-status-account-acct'] = account.acct
|
||||||
|
end
|
||||||
|
|
||||||
Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
|
Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
|
||||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||||
encode(entity[:url])
|
encode(entity[:url])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def apply_inner_link(html)
|
||||||
|
doc = Nokogiri::HTML.parse(html, nil, 'utf-8')
|
||||||
|
doc.css('a').map do |x|
|
||||||
|
status, account = url_to_holding_status_and_account(x['href'])
|
||||||
|
|
||||||
|
if account.present?
|
||||||
|
x.add_class('status-url-link')
|
||||||
|
x['data-status-id'] = status.id
|
||||||
|
x['data-status-account-acct'] = account.acct
|
||||||
|
end
|
||||||
|
end
|
||||||
|
html = doc.css('body')[0]&.inner_html || ''
|
||||||
|
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_to_holding_status_and_account(url)
|
||||||
|
url = url.split('#').first
|
||||||
|
|
||||||
|
return if url.nil?
|
||||||
|
|
||||||
|
EntityCache.instance.holding_status_and_account(url)
|
||||||
|
end
|
||||||
|
|
||||||
def link_to_mention(entity, linkable_accounts, options = {})
|
def link_to_mention(entity, linkable_accounts, options = {})
|
||||||
acct = entity[:screen_name]
|
acct = entity[:screen_name]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue