From 6e28a99c8e46295dd049f7af45565d4bea97c725 Mon Sep 17 00:00:00 2001 From: Fire Demon Date: Sun, 16 Aug 2020 03:24:47 -0500 Subject: [Feature] Full article support --- .../flavours/glitch/actions/importer/normalizer.js | 2 ++ .../flavours/glitch/components/status_content.js | 40 ++++++++++++++++++---- .../features/status/components/detailed_status.js | 1 + .../styles/monsterfork/components/status.scss | 19 +++++++--- app/javascript/mastodon/locales/en-MP.json | 2 ++ app/lib/command_tag/command/status_tools.rb | 22 ++++++++++-- app/lib/formatter.rb | 20 +++++++++-- app/lib/sanitize_config.rb | 19 ++++++++-- app/serializers/activitypub/note_serializer.rb | 10 ++++-- app/serializers/rest/status_serializer.rb | 18 +++++++++- app/views/statuses/_detailed_status.html.haml | 4 +-- 11 files changed, 135 insertions(+), 22 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 70431dce3..729c8d700 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -61,6 +61,7 @@ export function normalizeStatus(status, normalOldStatus) { if (normalOldStatus && oldUpdatedAt === newUpdatedAt) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); + normalStatus.articleHtml = normalOldStatus.get('articleHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); } else { const spoilerText = normalStatus.spoiler_text || ''; @@ -69,6 +70,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.articleHtml = normalStatus.article_content ? emojify(normalStatus.article_content, emojiMap) : normalStatus.contentHtml; normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); } diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index d34da1a8f..0d861b9c0 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -78,11 +78,13 @@ export default class StatusContent extends React.PureComponent { onUpdate: PropTypes.func, tagLinks: PropTypes.bool, rewriteMentions: PropTypes.string, + article: PropTypes.bool, }; static defaultProps = { tagLinks: true, rewriteMentions: 'no', + article: false, }; state = { @@ -272,6 +274,7 @@ export default class StatusContent extends React.PureComponent { disabled, tagLinks, rewriteMentions, + article, } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; @@ -324,12 +327,29 @@ export default class StatusContent extends React.PureComponent { ); + const article_content = status.get('article') && ( +
+ + + + +
+ ); + const status_notice_wrapper = (
{unpublished} {quiet} {edited} {local_only} + {article_content}
); @@ -402,7 +422,7 @@ export default class StatusContent extends React.PureComponent { ); - const content = { __html: status.get('contentHtml') }; + const content = { __html: article ? status.get('articleHtml') : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; const directionStyle = { direction: 'ltr' }; const classNames = classnames('status__content', { @@ -429,11 +449,19 @@ export default class StatusContent extends React.PureComponent { )).reduce((aggregate, item) => [...aggregate, item, ' '], []); const toggleText = hidden ? [ - , + article ? ( + + ) : ( + + ), mediaIcon ? ( diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss index 30cd5d668..73f883db1 100644 --- a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss @@ -8,19 +8,30 @@ } .status__notice { - & > span { - color: $dark-text-color; + display: flex; + align-items: center; + + & > span, & > a { + display: inline-flex; + align-items: center; line-height: normal; font-style: italic; + font-weight: bold; font-size: 12px; padding-left: 8px; - position: relative; - bottom: 0.2em; + height: 1.5em; + } + + & > span { + color: $dark-text-color; } & > i { + display: inline-flex; + align-items: center; color: lighten($dark-text-color, 4%); width: 1.1em; + height: 1.5em; } } diff --git a/app/javascript/mastodon/locales/en-MP.json b/app/javascript/mastodon/locales/en-MP.json index 2818b7003..ec331be2c 100644 --- a/app/javascript/mastodon/locales/en-MP.json +++ b/app/javascript/mastodon/locales/en-MP.json @@ -125,6 +125,7 @@ "settings.side_arm": "Secondary roar button:", "status.admin_account": "Moderate @{name}", "status.admin_status": "Moderate roar", + "status.article": "Article", "status.cannot_reblog": "This roar cannot be boosted", "status.copy": "Copy link to roar", "status.edit": "Edit", @@ -142,6 +143,7 @@ "status.publish": "Publish", "status.reblogged_by": "{name}", "status.reblogs.empty": "No one has boosted this roar yet. When someone does, they will show up here.", + "status.show_article": "Show article", "status.show_less_all": "Hide all", "status.show_less": "Hide", "status.show_more_all": "Reveal all", diff --git a/app/lib/command_tag/command/status_tools.rb b/app/lib/command_tag/command/status_tools.rb index 1727a956e..1cdb90e4a 100644 --- a/app/lib/command_tag/command/status_tools.rb +++ b/app/lib/command_tag/command/status_tools.rb @@ -1,11 +1,29 @@ # frozen_string_literal: true module CommandTag::Command::StatusTools + def handle_article_before_save(args) + return unless author_of_status? && args.present? + + case args.shift.downcase + when 'title', 'name', 't' + status.title = args.join(' ') + when 'summary', 'abstract', 'cw', 'cn', 's', 'a' + @status.title = @status.spoiler_text if @status.title.blank? + @status.spoiler_text = args.join(' ') + end + end + def handle_title_before_save(args) - return unless author_of_status? + args.unshift('title') + handle_article_before_save(args) + end - @status.title = args[0] + def handle_summary_before_save(args) + args.unshift('summary') + handle_article_before_save(args) end + alias handle_abstract_before_save handle_summary_before_save + def handle_visibility_before_save(args) return unless author_of_status? && args[0].present? diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index d85bbf0e0..bf99701b7 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -39,8 +39,14 @@ class Formatter prepend_reblog = false end + summary = nil raw_content = status.text + if status.title.present? + summary = status.spoiler_text.presence || status.text + raw_content = options[:article_content] ? status.text : summary + end + if options[:inline_poll_options] && status.preloadable_poll raw_content = raw_content + "\n\n" + status.preloadable_poll.options.map { |title| "[ ] #{title}" }.join("\n") end @@ -50,6 +56,7 @@ class Formatter unless status.local? html = reformat(raw_content) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] + html = format_article_content(summary, html) if options[:article_content] && summary.present? return html.html_safe # rubocop:disable Rails/OutputSafety end @@ -68,6 +75,7 @@ class Formatter html = html.delete("\n") end + html = format_article_content(summary, html) if options[:article_content] && summary.present? html.html_safe # rubocop:disable Rails/OutputSafety end @@ -77,10 +85,14 @@ class Formatter end def format_article(text) - text = text.gsub(/>[\r\n]+<") + text = text.gsub(/>[\r\n]+<') text.html_safe # rubocop:disable Rails/OutputSafety end + def format_article_content(summary, html) + "
#{format_summary(summary, html)}
#{html}" + end + def reformat(html, outgoing = false) sanitize(html, Sanitize::Config::MASTODON_STRICT.merge(outgoing: outgoing)) rescue ArgumentError @@ -108,8 +120,12 @@ class Formatter Sanitize.fragment(html, config) end + def format_summary(summary, fallback) + summary&.strip.presence || fallback[/(?:

.*?<\/p>)/im].presence || '🗎❓' + end + def format_spoiler(status, **options) - html = encode(status.spoiler_text) + html = encode(status.title.presence || status.spoiler_text) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) html.html_safe # rubocop:disable Rails/OutputSafety end diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb index 74a1d93fb..102dce2d2 100644 --- a/app/lib/sanitize_config.rb +++ b/app/lib/sanitize_config.rb @@ -30,12 +30,25 @@ class Sanitize next true if e =~ /^(h|p|u|dt|e)-/ # microformats classes next true if e =~ /^(mention|hashtag)$/ # semantic classes next true if e =~ /^(ellipsis|invisible)$/ # link formatting classes - next true if %w(center centered).include?(e) + next true if %w(center centered abstract).include?(e) end node['class'] = class_list.join(' ') end + DATA_NAME_ALLOWLIST_TRANSFORMER = lambda do |env| + node = env[:node] + name_list = node['data-name']&.split(/[\t\n\f\r ]/) + + return unless name_list + + name_list.keep_if do |name| + next true if %w(summary abstract).include?(name) + end + + node['data-name'] = name_list.join(' ') + end + LINK_REL_TRANSFORMER = lambda do |env| return unless env[:node_name] == 'a' and env[:node]['href'] @@ -71,10 +84,11 @@ class Sanitize 'a' => %w(href rel class title), 'span' => %w(class), 'abbr' => %w(title), - 'blockquote' => %w(cite), + 'blockquote' => %w(cite data-name), 'ol' => %w(start reversed), 'li' => %w(value), 'img' => %w(src alt title), + 'p' => %w(data-name), }, add_attributes: { @@ -90,6 +104,7 @@ class Sanitize transformers: [ CLASS_WHITELIST_TRANSFORMER, + DATA_NAME_ALLOWLIST_TRANSFORMER, UNSUPPORTED_HREF_TRANSFORMER, LINK_REL_TRANSFORMER, ] diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 3d99e29c4..163f25560 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -51,6 +51,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def summary + return Formatter.instance.format(object) if title_present? + object.spoiler_text.presence || (instance_options[:allow_local_only] ? nil : Setting.outgoing_spoilers.presence) end @@ -67,11 +69,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def content - Formatter.instance.format(object) + Formatter.instance.format(object, article_content: true) end def content_map - { object.language => Formatter.instance.format(object) } + { object.language => Formatter.instance.format(object, article_content: true) } end def replies @@ -193,7 +195,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def title_present? - object.title.present? + return @has_title if defined?(@has_title) + + @has_title = object.title.present? end def server_metadata diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 081b42979..dec39ec24 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -7,7 +7,7 @@ class REST::StatusSerializer < ActiveModel::Serializer :favourites_count # Monsterfork additions - attributes :updated_at, :edited, :nest_level, :title + attributes :updated_at, :edited, :nest_level attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? @@ -24,6 +24,8 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :hidden, if: :current_user? attribute :conversation_hidden, if: :current_user? attribute :notify, if: :locally_owned? + attribute :title?, key: :article + attribute :article_content, if: :title? belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application, if: :show_application? @@ -63,6 +65,12 @@ class REST::StatusSerializer < ActiveModel::Serializer object.local? && owned? end + def title? + return @has_title if defined?(@has_title) + + @has_title = object.title.present? + end + def show_application? object.account.user_shows_application? || owned? end @@ -82,10 +90,18 @@ class REST::StatusSerializer < ActiveModel::Serializer ActivityPub::TagManager.instance.uri_for(object) end + def spoiler_text + title? ? object.title : object.spoiler_text + end + def content Formatter.instance.format(object) end + def article_content + Formatter.instance.format(object, article_content: true) + end + def text object.original_text.presence || object.text end diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index b3e9c44fc..d3c538368 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -16,12 +16,12 @@ = account_action_button(status.account) .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - - if status.spoiler_text? + - if status.title? || status.spoiler_text? %p< %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  %button.status__content__spoiler-link= t('statuses.show_more') .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } - = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) + = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay, article_content: true) - if status.preloadable_poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do = render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } -- cgit