Adding full Article support

This creates a new column in the `statuses` table which keeps track of
activity_pub_type, so in the case of a Note it will be blank (the
default) and it will be a string "Article" if the received remote object
is an AP Article. There is now a bunch of special case code in the
formatters and sanitizers to handle Articles differently, as well as on
the clientside.
This commit is contained in:
Darius Kazemi 2019-05-05 16:59:04 -07:00
parent b3e65978b4
commit 0436aa9984
8 changed files with 34 additions and 9 deletions

View File

@ -409,7 +409,7 @@ class Status extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> <div className={classNames('status__wrapper', `status__wrapper-type-${status.get('activity_pub_type') || 'none'}` , `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>

View File

@ -170,6 +170,12 @@ export default class StatusContent extends React.PureComponent {
</button> </button>
); );
const readArticleButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
<FormattedMessage id='status.read_article' defaultMessage='Read article' /><Icon id='angle-right' fixedWidth />
</button>
);
if (status.get('spoiler_text').length > 0) { if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = ''; let mentionsPlaceholder = '';
@ -185,21 +191,26 @@ export default class StatusContent extends React.PureComponent {
mentionsPlaceholder = <div>{mentionLinks}</div>; mentionsPlaceholder = <div>{mentionLinks}</div>;
} }
return ( const output = [
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} /> <span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} />
{' '} {' '}
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button> {status.get('activity_pub_type') === 'Article' ? '' : <div><button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button></div>}
</p> </p>
{mentionsPlaceholder} {mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} /> <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />} {!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div> </div>,
); ];
if (status.get('activity_pub_type') === 'Article' && !this.props.expanded) {
output.push(readArticleButton);
}
return output;
} else if (this.props.onClick) { } else if (this.props.onClick) {
const output = [ const output = [
<div <div

View File

@ -213,7 +213,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
<DisplayName account={status.get('account')} localDomain={this.props.domain} /> <DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a> </a>
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> <StatusContent status={status} expanded={status.get('activity_pub_type') === 'Article' || !status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
{media} {media}

View File

@ -773,6 +773,7 @@
ul, ul,
ol { ol {
margin-left: 1em; margin-left: 1em;
margin-bottom: 1em;
p { p {
margin: 0; margin: 0;
@ -840,6 +841,10 @@
display: block; display: block;
} }
} }
.article-type img {
max-width: 95%;
}
} }
.status__content.status__content--collapsed { .status__content.status__content--collapsed {

View File

@ -60,7 +60,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
account: @account, account: @account,
text: text_from_content || '', text: text_from_content || '',
language: detected_language, language: detected_language,
spoiler_text: converted_object_type? ? '' : (text_from_summary || ''), spoiler_text: converted_object_type? ? '' : (text_from_summary || (@object['type'] == 'Article' && text_from_name) || ''),
created_at: @object['published'], created_at: @object['published'],
override_timestamps: @options[:override_timestamps], override_timestamps: @options[:override_timestamps],
reply: @object['inReplyTo'].present?, reply: @object['inReplyTo'].present?,
@ -70,6 +70,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
conversation: conversation_from_uri(@object['conversation']), conversation: conversation_from_uri(@object['conversation']),
media_attachment_ids: process_attachments.take(4).map(&:id), media_attachment_ids: process_attachments.take(4).map(&:id),
poll: process_poll, poll: process_poll,
activity_pub_type: @object['type']
} }
end end
end end

View File

@ -92,6 +92,7 @@ class Formatter
def format_article(text) def format_article(text)
text = text.gsub(/>\n+</, "><") text = text.gsub(/>\n+</, "><")
text = "<span class='article-type'>#{text}</span>"
text.html_safe # rubocop:disable Rails/OutputSafety text.html_safe # rubocop:disable Rails/OutputSafety
end end

View File

@ -49,6 +49,9 @@ class Sanitize
'rel' => 'nofollow noopener', 'rel' => 'nofollow noopener',
'target' => '_blank', 'target' => '_blank',
}, },
'span' => {
'class' => 'article-type',
},
}, },
protocols: { protocols: {

View File

@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
:sensitive, :spoiler_text, :visibility, :language, :sensitive, :spoiler_text, :visibility, :language,
:uri, :url, :replies_count, :reblogs_count, :uri, :url, :replies_count, :reblogs_count,
:favourites_count, :local_only :favourites_count, :local_only, :activity_pub_type
attribute :favourited, if: :current_user? attribute :favourited, if: :current_user?
attribute :reblogged, if: :current_user? attribute :reblogged, if: :current_user?
@ -61,6 +61,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
OStatus::TagManager.instance.uri_for(object) OStatus::TagManager.instance.uri_for(object)
end end
def activity_pub_type
object.activity_pub_type.to_s
end
def content def content
Formatter.instance.format(object) Formatter.instance.format(object)
end end