Add support for `fediverse:creator` OpenGraph tag (#30398)
parent
4a77e477ee
commit
128987eded
|
@ -38,15 +38,15 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
def paginated_conversations
|
def paginated_conversations
|
||||||
AccountConversation.where(account: current_account)
|
AccountConversation.where(account: current_account)
|
||||||
.includes(
|
.includes(
|
||||||
account: :account_stat,
|
account: [:account_stat, user: :role],
|
||||||
last_status: [
|
last_status: [
|
||||||
:media_attachments,
|
:media_attachments,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:tags,
|
:tags,
|
||||||
{
|
{
|
||||||
preview_cards_status: :preview_card,
|
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||||
active_mentions: [account: :account_stat],
|
active_mentions: :account,
|
||||||
account: :account_stat,
|
account: [:account_stat, user: :role],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,8 @@ import { PureComponent } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
@ -13,6 +15,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
||||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
||||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
||||||
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
@ -56,6 +59,20 @@ const addAutoPlay = html => {
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MoreFromAuthor = ({ author }) => (
|
||||||
|
<div className='more-from-author'>
|
||||||
|
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
||||||
|
<use xlinkHref='#logo-symbol-icon' />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <Link to={`/@${author.get('acct')}`}><Avatar account={author} size={16} /> {author.get('display_name')}</Link> }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
MoreFromAuthor.propTypes = {
|
||||||
|
author: ImmutablePropTypes.map,
|
||||||
|
};
|
||||||
|
|
||||||
export default class Card extends PureComponent {
|
export default class Card extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -136,6 +153,7 @@ export default class Card extends PureComponent {
|
||||||
const interactive = card.get('type') === 'video';
|
const interactive = card.get('type') === 'video';
|
||||||
const language = card.get('language') || '';
|
const language = card.get('language') || '';
|
||||||
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
|
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
|
||||||
|
const showAuthor = !!card.get('author_account');
|
||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<div className='status-card__content'>
|
<div className='status-card__content'>
|
||||||
|
@ -146,7 +164,7 @@ export default class Card extends PureComponent {
|
||||||
|
|
||||||
<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
|
<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
|
||||||
|
|
||||||
{card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>}
|
{!showAuthor && (card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -235,10 +253,14 @@ export default class Card extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
|
<>
|
||||||
|
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage, bottomless: showAuthor })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
|
||||||
{embed}
|
{embed}
|
||||||
{description}
|
{description}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{showAuthor && <MoreFromAuthor author={card.get('author_account')} />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -414,6 +414,7 @@
|
||||||
"limited_account_hint.action": "Show profile anyway",
|
"limited_account_hint.action": "Show profile anyway",
|
||||||
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
|
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
|
||||||
"link_preview.author": "By {name}",
|
"link_preview.author": "By {name}",
|
||||||
|
"link_preview.more_from_author": "More from {name}",
|
||||||
"lists.account.add": "Add to list",
|
"lists.account.add": "Add to list",
|
||||||
"lists.account.remove": "Remove from list",
|
"lists.account.remove": "Remove from list",
|
||||||
"lists.delete": "Delete list",
|
"lists.delete": "Delete list",
|
||||||
|
|
|
@ -3896,6 +3896,10 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
border: 1px solid var(--background-border-color);
|
border: 1px solid var(--background-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&.bottomless {
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
|
@ -10223,3 +10227,42 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-from-author {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
background: var(--surface-background-color);
|
||||||
|
border: 1px solid var(--background-border-color);
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 16px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $primary-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -195,6 +195,10 @@ class LinkDetailsExtractor
|
||||||
structured_data&.author_url
|
structured_data&.author_url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def author_account
|
||||||
|
opengraph_tag('fediverse:creator')
|
||||||
|
end
|
||||||
|
|
||||||
def embed_url
|
def embed_url
|
||||||
valid_url_or_nil(opengraph_tag('twitter:player:stream'))
|
valid_url_or_nil(opengraph_tag('twitter:player:stream'))
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
# link_type :integer
|
# link_type :integer
|
||||||
# published_at :datetime
|
# published_at :datetime
|
||||||
# image_description :string default(""), not null
|
# image_description :string default(""), not null
|
||||||
|
# author_account_id :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class PreviewCard < ApplicationRecord
|
class PreviewCard < ApplicationRecord
|
||||||
|
@ -54,6 +55,7 @@ class PreviewCard < ApplicationRecord
|
||||||
has_many :statuses, through: :preview_cards_statuses
|
has_many :statuses, through: :preview_cards_statuses
|
||||||
|
|
||||||
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
|
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
|
||||||
|
belongs_to :author_account, class_name: 'Account', optional: true
|
||||||
|
|
||||||
has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false
|
has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false
|
||||||
|
|
||||||
|
|
|
@ -157,9 +157,9 @@ class Status < ApplicationRecord
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:tags,
|
:tags,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
preview_cards_status: [:preview_card],
|
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: :account,
|
||||||
reblog: [
|
reblog: [
|
||||||
:application,
|
:application,
|
||||||
:tags,
|
:tags,
|
||||||
|
@ -167,11 +167,11 @@ class Status < ApplicationRecord
|
||||||
:conversation,
|
:conversation,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
preview_cards_status: [:preview_card],
|
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: :account,
|
||||||
],
|
],
|
||||||
thread: { account: :account_stat }
|
thread: :account
|
||||||
|
|
||||||
delegate :domain, to: :account, prefix: true
|
delegate :domain, to: :account, prefix: true
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
||||||
:provider_url, :html, :width, :height,
|
:provider_url, :html, :width, :height,
|
||||||
:image, :image_description, :embed_url, :blurhash, :published_at
|
:image, :image_description, :embed_url, :blurhash, :published_at
|
||||||
|
|
||||||
|
has_one :author_account, serializer: REST::AccountSerializer, if: -> { object.author_account.present? }
|
||||||
|
|
||||||
def url
|
def url
|
||||||
object.original_url.presence || object.url
|
object.original_url.presence || object.url
|
||||||
end
|
end
|
||||||
|
|
|
@ -147,9 +147,12 @@ class FetchLinkCardService < BaseService
|
||||||
return if html.nil?
|
return if html.nil?
|
||||||
|
|
||||||
link_details_extractor = LinkDetailsExtractor.new(@url, @html, @html_charset)
|
link_details_extractor = LinkDetailsExtractor.new(@url, @html, @html_charset)
|
||||||
|
provider = PreviewCardProvider.matching_domain(Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host)
|
||||||
|
linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present? && provider&.trendable?
|
||||||
|
|
||||||
@card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
|
@card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
|
||||||
@card.assign_attributes(link_details_extractor.to_preview_card_attributes)
|
@card.assign_attributes(link_details_extractor.to_preview_card_attributes)
|
||||||
|
@card.author_account = linked_account
|
||||||
@card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
|
@card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddAuthorAccountIdToPreviewCards < ActiveRecord::Migration[7.1]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
safety_assured { add_reference :preview_cards, :author_account, null: true, foreign_key: { to_table: 'accounts', on_delete: :nullify }, index: false }
|
||||||
|
add_index :preview_cards, :author_account_id, algorithm: :concurrently, where: 'author_account_id IS NOT NULL'
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_05_10_192043) do
|
ActiveRecord::Schema[7.1].define(version: 2024_05_22_041528) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -877,6 +877,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_10_192043) do
|
||||||
t.integer "link_type"
|
t.integer "link_type"
|
||||||
t.datetime "published_at"
|
t.datetime "published_at"
|
||||||
t.string "image_description", default: "", null: false
|
t.string "image_description", default: "", null: false
|
||||||
|
t.bigint "author_account_id"
|
||||||
|
t.index ["author_account_id"], name: "index_preview_cards_on_author_account_id", where: "(author_account_id IS NOT NULL)"
|
||||||
t.index ["url"], name: "index_preview_cards_on_url", unique: true
|
t.index ["url"], name: "index_preview_cards_on_url", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1352,6 +1354,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_10_192043) do
|
||||||
add_foreign_key "polls", "accounts", on_delete: :cascade
|
add_foreign_key "polls", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "polls", "statuses", on_delete: :cascade
|
add_foreign_key "polls", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade
|
add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade
|
||||||
|
add_foreign_key "preview_cards", "accounts", column: "author_account_id", on_delete: :nullify
|
||||||
add_foreign_key "report_notes", "accounts", on_delete: :cascade
|
add_foreign_key "report_notes", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "report_notes", "reports", on_delete: :cascade
|
add_foreign_key "report_notes", "reports", on_delete: :cascade
|
||||||
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify
|
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify
|
||||||
|
|
Loading…
Reference in New Issue