diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 7547e4357..5332b2172 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -9,6 +9,7 @@ import Icon from 'mastodon/components/icon'; import { autoPlayGif, show_reply_tree_button } from 'mastodon/initial_state'; const messages = defineMessages({ + linkToAcct: { id: 'status.link_to_acct', defaultMessage: 'Link to @{acct}' }, postByAcct: { id: 'status.post_by_acct', defaultMessage: 'Post by @{acct}' }, }); @@ -64,6 +65,9 @@ export default class StatusContent extends React.PureComponent { link.setAttribute('title', mention.get('acct')); } 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); + } else if (link.classList.contains('account-url-link')) { + link.setAttribute('title', this.props.intl.formatMessage(messages.linkToAcct, { acct: link.dataset.accountAcct })); + link.addEventListener('click', this.onAccountUrlClick.bind(this, link.dataset.accountId, link.dataset.accountActorType), 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); @@ -146,6 +150,13 @@ export default class StatusContent extends React.PureComponent { } } + onAccountUrlClick = (accountId, accountActorType, e) => { + if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`${accountActorType == 'Group' ? '/timelines/groups/' : '/accounts/'}${accountId}`); + } + } + onStatusUrlClick = (statusId, e) => { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index eed53f4eb..71d7a73c2 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -500,6 +500,7 @@ "status.emoji_reaction": "Emoji reaction", "status.favourite": "Favourite", "status.filtered": "Filtered", + "status.link_to_acct": "Link to @{acct}", "status.load_more": "Load more", "status.media_hidden": "Media hidden", "status.mention": "Mention @{name}", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index a238469be..e4b9cfe87 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -501,6 +501,7 @@ "status.emoji_reaction": "絵文字リアクション", "status.favourite": "お気に入り", "status.filtered": "フィルターされました", + "status.link_to_acct": "@{acct}へのリンク", "status.load_more": "もっと見る", "status.media_hidden": "非表示のメディア", "status.mention": "@{name}さんに投稿", diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb index bbc8d46ca..4f3bc5c25 100644 --- a/app/lib/entity_cache.rb +++ b/app/lib/entity_cache.rb @@ -35,6 +35,30 @@ class EntityCache shortcodes.filter_map { |shortcode| cached[to_key(:emoji, shortcode, domain)] || uncached[shortcode] } end + def holding_account(url) + return Rails.cache.read(to_key(:holding_account, url)) if Rails.cache.exist?(to_key(:holding_account, url)) + + account = begin + if ActivityPub::TagManager.instance.local_uri?(url) + recognized_params = Rails.application.routes.recognize_path(url) + + return nil unless recognized_params[:action] == 'show' + + if recognized_params[:controller] == 'accounts' + Account.find_local(recognized_params[:username]) + end + else + Account.where(uri: url).or(Account.where(url: url)).first + end + rescue ActiveRecord::RecordNotFound + nil + end + + Rails.cache.write(to_key(:holding_account, url), account, expires_in: account.nil? ? MIN_EXPIRATION : MAX_EXPIRATION) + + account + 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)) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 2b7303405..bb1ec029d 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -243,6 +243,10 @@ class Formatter Extractor.remove_overlapping_entities(special + standard + extra) end + def class_append(c, items) + (c || '').split.concat(items).uniq.join(' ') + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' } @@ -250,11 +254,17 @@ class Formatter html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me] status, account = url_to_holding_status_and_account(url.normalize.to_s) + account = url_to_holding_account(url.normalize.to_s) if status.nil? - if account.present? - html_attrs[:class] = 'status-url-link' - html_attrs[:'data-status-id'] = status.id + if status.present? && account.present? + html_attrs[:class] = class_append(html_attrs[:class], ['status-url-link']) + html_attrs[:'data-status-id'] = status.id html_attrs[:'data-status-account-acct'] = account.acct + elsif account.present? + html_attrs[:class] = class_append(html_attrs[:class], ['account-url-link']) + html_attrs[:'data-account-id'] = account.id + html_attrs[:'data-account-actor-type'] = account.actor_type + html_attrs[:'data-account-acct'] = account.acct end Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs) @@ -266,17 +276,31 @@ class Formatter doc = Nokogiri::HTML.parse(html, nil, 'utf-8') doc.css('a').map do |x| status, account = url_to_holding_status_and_account(x['href']) + account = url_to_holding_account(x['href']) if status.nil? - if account.present? + if status.present? && account.present? x.add_class('status-url-link') - x['data-status-id'] = status.id + x['data-status-id'] = status.id x['data-status-account-acct'] = account.acct + elsif account.present? + x.add_class('account-url-link') + x['data-account-id'] = account.id + x['data-account-actor-type'] = account.actor_type + x['data-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_account(url) + url = url.split('#').first + + return if url.nil? + + EntityCache.instance.holding_account(url) + end + def url_to_holding_status_and_account(url) url = url.split('#').first