Change to treat the url to a post already owned as an internal link

This commit is contained in:
noellabo 2021-09-24 00:09:04 +09:00
parent ff063001d5
commit bd183630a4
5 changed files with 73 additions and 2 deletions

View file

@ -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}`;

View file

@ -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",

View file

@ -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": "ブースト",

View file

@ -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

View file

@ -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]