diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 8c9c2aa3a11c7bf314fad3ff74c78b7557697de2..e71fd1b6729b9437f3a3a6b4b93407ab4897d67b 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -66,6 +66,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_show_quote_button, :setting_show_bookmark_button, :setting_show_target, + :setting_strip_formatting, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index adb7918c53fd14b867d5b958744f0c4173f17cc3..694cdff6c197a31d33022c401c157d674d74eeb0 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -61,6 +61,17 @@ module StatusesHelper embedded_view? ? '_blank' : nil end + def text_formatting_classes + case current_user&.setting_strip_formatting + when 'none', nil + 'rich-text rich-blocks' + when 'blocks' + 'rich-text' + when 'all' + nil + end + end + def style_classes(status, is_predecessor, is_successor, include_threads) classes = ['entry'] classes << 'entry-predecessor' if is_predecessor diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 60f8b1c7e09d49ca750c0087392ab3e7ae843db2..5cbfd6c0b5e1956f523250ed51c9203a2828fe79 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; import { autoPlayGif } from 'mastodon/initial_state'; +import { stripFormatting } from 'mastodon/initial_state'; const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) @@ -210,6 +211,8 @@ export default class StatusContent extends React.PureComponent { 'status__content--with-action': this.props.onClick && this.context.router, 'status__content--with-spoiler': status.get('spoiler_text').length > 0, 'status__content--collapsed': renderReadMore, + 'rich-text': stripFormatting !== 'all', + 'rich-blocks': stripFormatting === 'none', }); if (isRtl(status.get('search_index'))) { diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 289c02148334a7343fe09728f3104e6500047381..487e09e0a93861237b814996966da044a5b792d0 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -35,5 +35,6 @@ export const show_navigation_panel = getMeta('show_navigation_panel'); export const show_quote_button = getMeta('show_quote_button'); export const show_bookmark_button = getMeta('show_bookmark_button'); export const show_target = getMeta('show_target'); +export const stripFormatting = getMeta('strip_formatting'); export default initialState; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5e0c090f516fb1f9f78d144d3ad5d322c7db200c..5e28b0961cd1ef7e5b4264ee20013587d97caf07 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -843,10 +843,6 @@ &.status__content--with-spoiler { white-space: normal; - - .status__content__text { - white-space: pre-wrap; - } } .emojione { @@ -855,7 +851,9 @@ margin: -3px 0 0; } - p { + p, + pre, + blockquote { margin-bottom: 20px; white-space: pre-wrap; @@ -864,6 +862,175 @@ } } + h1, + h2, + h3, + h4, + h5 { + margin-bottom: 20px; + } + + blockquote { + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + ul, + ol { + p { + margin-bottom: 0; + } + } + + &:not(.rich-text) { + del { + text-decoration: none; + + &::before, + &::after { + content: '~~'; + } + } + + code { + font-family: inherit; + } + + u { + text-decoration: none; + + &::before, + &::after { + content: '__'; + } + } + + h1::before { + content: '# '; + } + + h2::before { + content: '## '; + } + + h3::before { + content: '### '; + } + + h4::before { + content: '#### '; + } + + h5::before { + content: '##### '; + } + + b, + strong { + &::before, + &::after { + content: '**'; + } + } + + em, + i { + &::before, + &::after { + content: '*'; + } + } + } + + &:not(.rich-blocks) { + blockquote { + position: relative; + padding-left: 1em; + overflow: hidden; + } + + blockquote::before { + position: absolute; + content: '>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a>\a'; + white-space: pre-wrap; + left: 0; + top: 0; + } + + li::before { + position: absolute; + content: '*'; + left: 0; + top: 0; + } + + li { + position: relative; + padding-left: 1em; + } + } + + &.rich-text { + h1, + h2 { + font-weight: 700; + } + + h3, + h4, + h5 { + font-weight: 500; + } + + b, + strong { + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + sub { + font-size: smaller; + text-align: sub; + } + } + + &.rich-blocks { + h1, + h2 { + font-size: 18px; + } + + h2 { + font-size: 16px; + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + } + + ul, + ol { + margin-left: 1em; + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } + } + a { color: $secondary-text-color; text-decoration: none; diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb index 04929d311557f089bd01f1d3c742f65ae0439047..545a53d518d261dacbcb336840830ef68628d8f1 100644 --- a/app/lib/sanitize_config.rb +++ b/app/lib/sanitize_config.rb @@ -72,11 +72,13 @@ class Sanitize end MASTODON_STRICT ||= freeze_config( - elements: %w(p br span a), + elements: %w(p br span a abbr del pre blockquote code b strong u sub i em h1 h2 h3 h4 h5 ul ol li), attributes: { - 'a' => %w(href rel class), - 'span' => %w(class), + 'a' => %w(href rel class title), + 'span' => %w(class), + 'abbr' => %w(title), + 'blockquote' => %w(cite), }, add_attributes: { @@ -86,7 +88,10 @@ class Sanitize }, }, - protocols: {}, + protocols: { + 'a' => { 'href' => HTTP_PROTOCOLS }, + 'blockquote' => { 'cite' => HTTP_PROTOCOLS }, + }, transformers: [ CLASS_WHITELIST_TRANSFORMER, diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index de5c639feccf6dd65fe4d111776a0a489a30bf5d..845f684fa252cc440afaa959a4d0e35705d01d79 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -50,6 +50,7 @@ class UserSettingsDecorator user.settings['show_quote_button'] = show_quote_button_preference if change?('setting_show_quote_button') user.settings['show_bookmark_button'] = show_bookmark_button_preference if change?('setting_show_bookmark_button') user.settings['show_target'] = show_target_preference if change?('setting_show_target') + user.settings['strip_formatting'] = strip_formatting_preference if change?('setting_strip_formatting') end def merged_notification_emails @@ -71,7 +72,7 @@ class UserSettingsDecorator def default_federation_preference boolean_cast_setting 'setting_default_federation' end - + def default_content_type_preference settings['setting_default_content_type'] end @@ -192,6 +193,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_show_target' end + def strip_formatting_preference + settings['setting_strip_formatting'] + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 83a617f3c82d629e0fe726f9b6697df8a139b91d..13770c35e6ac30593db8f7390df12e5ce50ded89 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -120,7 +120,7 @@ class User < ApplicationRecord :show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target, :show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_followed_by, :show_target, :follow_button_to_list_adder, :show_navigation_panel, :show_quote_button, :show_bookmark_button, - to: :settings, prefix: :setting, allow_nil: false + :strip_formatting, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code, :sign_in_token_attempt attr_writer :external diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index b6390c23234a958af82b8e04310b972aa3c89a15..afdd2db2ae749840b5d1f687d4944f3d751ab98a 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -43,6 +43,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_pending_items] = object.current_account.user.setting_use_pending_items store[:is_staff] = object.current_account.user.staff? + store[:strip_formatting] = object.current_account.user.setting_strip_formatting store[:trends] = Setting.trends && object.current_account.user.setting_trends store[:crop_images] = object.current_account.user.setting_crop_images store[:show_follow_button_on_timeline] = object.current_account.user.setting_show_follow_button_on_timeline diff --git a/app/serializers/rest/preferences_serializer.rb b/app/serializers/rest/preferences_serializer.rb index 119f0e06d89a5831ea7702478bdb8a05b2934194..52daf37caadc922d5add7dac8c228f5a07f7db3d 100644 --- a/app/serializers/rest/preferences_serializer.rb +++ b/app/serializers/rest/preferences_serializer.rb @@ -7,6 +7,7 @@ class REST::PreferencesSerializer < ActiveModel::Serializer attribute :reading_default_sensitive_media, key: 'reading:expand:media' attribute :reading_default_sensitive_text, key: 'reading:expand:spoilers' + attribute :reading_strip_formatting, key: 'reading:formatting:strip' def posting_default_privacy object.user.setting_default_privacy @@ -27,4 +28,8 @@ class REST::PreferencesSerializer < ActiveModel::Serializer def reading_default_sensitive_text object.user.setting_expand_spoilers end + + def reading_strip_formatting + object.user.setting_strip_formatting + end end diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 91baca1b4068e0006e2129693240e475ea93eebc..4eb682d629c610765d7318db8c5a5fdc7e630e63 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -61,6 +61,9 @@ .fields-group = f.input :setting_show_bookmark_button, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_strip_formatting, collection: ['none', 'blocks', 'all'], wrapper: :with_floating_label, include_blank: false, label_method: lambda { |value| safe_join([I18n.t("statuses.strip_formatting.#{value}"), content_tag(:span, I18n.t("statuses.strip_formatting.#{value}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', hint: false + -# .fields-group -# = f.input :setting_show_target, as: :boolean, wrapper: :with_label diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index e27e84da84f74b28573ab1370a01617d5d3c74b3..d5ae1f01bfba16ddcc485fcdfb0465cb5f52bd79 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -15,7 +15,7 @@ = account_action_button(status.account) - .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< + .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?), class: text_formatting_classes }< - if status.spoiler_text? %p< %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)} diff --git a/app/views/statuses/_quote_status.html.haml b/app/views/statuses/_quote_status.html.haml index 373eff5aa2c9a7b159313b2e346731bb1cc0fa17..fa3c759f2d55736ce82e7cb918cdcc1003605ba5 100644 --- a/app/views/statuses/_quote_status.html.haml +++ b/app/views/statuses/_quote_status.html.haml @@ -11,14 +11,14 @@ = acct(status.account) = fa_icon('lock') if status.account.locked? - .status__content.emojify< + .status__content.emojify{ class: text_formatting_classes }< - if status.spoiler_text? %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< %span.p-summary> #{Formatter.instance.format_spoiler(status)} %button.status__content__spoiler-link= t('statuses.show_more') .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } = Formatter.instance.format_in_quote(status, custom_emojify: true) - + - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index c83a8bad03ec747ed98f3cb74e75367a5da3315d..17777eea1e0ab949545d157a211f7fa5ef6effac 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -21,7 +21,7 @@ %span.display-name__account = acct(status.account) = fa_icon('lock') if status.account.locked? - .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< + .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?), class: text_formatting_classes }< - if status.spoiler_text? %p< %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)} diff --git a/config/locales/en.yml b/config/locales/en.yml index b98283c1944e2a3fa6fb70ead648ddf0eb943dda..adb962cc907389002acbb965a9f67f4db7b239db 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1302,6 +1302,13 @@ en: show_more: Show more show_thread: Show thread sign_in_to_participate: Sign in to participate in the conversation + strip_formatting: + all: All + all_long: Strip all advanced formatting + blocks: Block elements + blocks_long: Strip formatting for title headers and block quotes + none: None + none_long: Do not strip any formatting supported by Mastodon title: '%{name}: "%{quote}"' visibilities: direct: Direct diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4bff199bc5ad4b9550cdc20a671e95707613943a..49f57b74706892e8fd58a3021aa8c44506537eea 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -187,6 +187,7 @@ en: setting_show_subscribe_button_on_timeline: Show subscribe button on timeline setting_show_followed_by: Reflect the following status on the follow button setting_show_target: Enable targeting features + setting_strip_formatting: Strip advanced formatting from toots setting_system_font_ui: Use system's default font setting_theme: Site theme setting_trends: Show today's trends diff --git a/config/settings.yml b/config/settings.yml index a044caf06723b4b8053d32deab7a22d3f6060152..a1b0d06e63dd66dc20c38c517f4f6f786dc526da 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -35,7 +35,6 @@ defaults: &defaults noindex: false theme: 'default' aggregate_reblogs: true - advanced_layout: false use_blurhash: true use_pending_items: false trends: true